8 Commits

Author SHA1 Message Date
Matt Nadareski
32e8bab2a6 Math is hard 2019-04-08 21:08:36 -07:00
Matt Nadareski
bd1c6f8b51 Processing in right files 2019-04-08 20:28:14 -07:00
Matt Nadareski
30da008cec Processing shunting 2019-04-08 02:09:33 -07:00
Matt Nadareski
aa7566c312 Separate out helper methods 2019-04-08 01:13:51 -07:00
Matt Nadareski
56a5f4951b Set proper keys 2019-04-08 01:07:08 -07:00
Matt Nadareski
4b71cd621f Code shared too much 2019-04-08 00:55:57 -07:00
Matt Nadareski
4042b1c216 Rename IO, move common case 2019-04-08 00:17:37 -07:00
Matt Nadareski
26c7a34e98 Read and use full headers 2019-04-08 00:09:18 -07:00
7 changed files with 754 additions and 546 deletions

View File

@@ -50,6 +50,7 @@
<Compile Include="Data\Constants.cs" />
<Compile Include="Headers\NCCHHeaderFlags.cs" />
<Compile Include="Headers\RomFSHeader.cs" />
<Compile Include="Helper.cs" />
<Compile Include="ThreeDSTool.cs" />
<Compile Include="Data\Enums.cs" />
<Compile Include="Headers\AccessControlInfo.cs" />

View File

@@ -1,5 +1,7 @@
using System.IO;
using System;
using System.IO;
using System.Linq;
using System.Numerics;
using ThreeDS.Data;
namespace ThreeDS.Headers
@@ -8,10 +10,20 @@ namespace ThreeDS.Headers
{
private const string NCCHMagicNumber = "NCCH";
/// <summary>
/// Partition number for the current partition
/// </summary>
public int PartitionNumber { get; set; }
/// <summary>
/// Partition table entry for the current partition
/// </summary>
public PartitionTableEntry Entry { get; set; }
/// <summary>
/// RSA-2048 signature of the NCCH header, using SHA-256.
/// </summary>
public byte[] RSA2048Signature = new byte[0x100];
public byte[] RSA2048Signature { get; private set; }
/// <summary>
/// Content size, in media units (1 media unit = 0x200 bytes)
@@ -26,6 +38,31 @@ namespace ThreeDS.Headers
public byte[] ExeFSIV { get { return PartitionId.Concat(Constants.ExefsCounter).ToArray(); } }
public byte[] RomFSIV { get { return PartitionId.Concat(Constants.RomfsCounter).ToArray(); } }
/// <summary>
/// Boot rom key
/// </summary>
private BigInteger KeyX;
/// <summary>
/// NCCH boot rom key
/// </summary>
private BigInteger KeyX2C;
/// <summary>
/// Kernel9/Process9 key
/// </summary>
private BigInteger KeyY;
/// <summary>
/// Normal AES key
/// </summary>
private BigInteger NormalKey;
/// <summary>
/// NCCH AES key
/// </summary>
private BigInteger NormalKey2C;
/// <summary>
/// Maker code
/// </summary>
@@ -160,14 +197,16 @@ namespace ThreeDS.Headers
/// Read from a stream and get an NCCH header, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="readSignature">True if the RSA signature is read, false otherwise</param>
/// <returns>NCCH header object, null on error</returns>
public static NCCHHeader Read(BinaryReader reader)
public static NCCHHeader Read(BinaryReader reader, bool readSignature)
{
NCCHHeader header = new NCCHHeader();
try
{
header.RSA2048Signature = reader.ReadBytes(0x100);
if (readSignature)
header.RSA2048Signature = reader.ReadBytes(0x100);
if (new string(reader.ReadChars(4)) != NCCHMagicNumber)
return null;
@@ -207,5 +246,356 @@ namespace ThreeDS.Headers
return null;
}
}
/// <summary>
/// Process a single partition
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="header">NCSD header representing the 3DS file</param>
/// <param name="encrypt">True if we want to encrypt the partitions, false otherwise</param>
/// <param name="development">True if development keys should be used, false otherwise</param>
public void ProcessPartition(BinaryReader reader, BinaryWriter writer, NCSDHeader header, bool encrypt, bool development)
{
// Check if the 'NoCrypto' bit is set
if (Flags.PossblyDecrypted ^ encrypt)
{
Console.WriteLine($"Partition {PartitionNumber}: Already " + (encrypt ? "Encrypted" : "Decrypted") + "?...");
return;
}
// Determine the Keys to be used
SetEncryptionKeys(header.BackupHeader.Flags, encrypt, development);
// Process each of the pieces if they exist
ProcessExtendedHeader(reader, writer, header.MediaUnitSize, encrypt);
ProcessExeFS(reader, writer, header.MediaUnitSize, encrypt);
ProcessRomFS(reader, writer, header.MediaUnitSize, header.BackupHeader.Flags, encrypt, development);
// Write out new CryptoMethod and BitMask flags
UpdateCryptoAndMasks(reader, writer, header, encrypt);
}
/// <summary>
/// Determine the set of keys to be used for encryption or decryption
/// </summary>
/// <param name="backupFlags">File backup flags for encryption</param>
/// <param name="encrypt">True if we're encrypting the file, false otherwise</param>
/// <param name="development">True if development keys should be used, false otherwise</param>
private void SetEncryptionKeys(NCCHHeaderFlags backupFlags, bool encrypt, bool development)
{
KeyX = 0;
KeyX2C = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
// Backup headers can't have a KeyY value set
if (RSA2048Signature != null)
KeyY = new BigInteger(RSA2048Signature.Take(16).Reverse().ToArray());
else
KeyY = new BigInteger(0);
NormalKey = 0;
NormalKey2C = Helper.RotateLeft((Helper.RotateLeft(KeyX2C, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
// Set the header to use based on mode
BitMasks masks = 0;
CryptoMethod method = 0;
if (encrypt)
{
masks = backupFlags.BitMasks;
method = backupFlags.CryptoMethod;
}
else
{
masks = Flags.BitMasks;
method = Flags.CryptoMethod;
}
if ((masks & BitMasks.FixedCryptoKey) != 0)
{
NormalKey = 0x00;
NormalKey2C = 0x00;
Console.WriteLine("Encryption Method: Zero Key");
}
else
{
if (method == CryptoMethod.Original)
{
KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
Console.WriteLine("Encryption Method: Key 0x2C");
}
else if (method == CryptoMethod.Seven)
{
KeyX = (development ? Constants.KeyX0x25 : Constants.KeyX0x25);
Console.WriteLine("Encryption Method: Key 0x25");
}
else if (method == CryptoMethod.NineThree)
{
KeyX = (development ? Constants.DevKeyX0x18 : Constants.KeyX0x18);
Console.WriteLine("Encryption Method: Key 0x18");
}
else if (method == CryptoMethod.NineSix)
{
KeyX = (development ? Constants.DevKeyX0x1B : Constants.KeyX0x1B);
Console.WriteLine("Encryption Method: Key 0x1B");
}
NormalKey = Helper.RotateLeft((Helper.RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
}
/// <summary>
/// Process the extended header, if it exists
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="mediaUnitSize">Number of bytes per media unit</param>
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
private bool ProcessExtendedHeader(BinaryReader reader, BinaryWriter writer, uint mediaUnitSize, bool encrypt)
{
if (ExtendedHeaderSizeInBytes > 0)
{
reader.BaseStream.Seek((Entry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin);
writer.BaseStream.Seek((Entry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + ": ExHeader");
var cipher = Helper.CreateAESCipher(NormalKey2C, PlainIV, encrypt);
writer.Write(cipher.ProcessBytes(reader.ReadBytes(Constants.CXTExtendedDataHeaderLength)));
writer.Flush();
return true;
}
else
{
Console.WriteLine($"Partition {PartitionNumber} ExeFS: No Extended Header... Skipping...");
return false;
}
}
/// <summary>
/// Process the ExeFS, if it exists
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="mediaUnitSize">Number of bytes per media unit</param>
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
private void ProcessExeFS(BinaryReader reader, BinaryWriter writer, uint mediaUnitSize, bool encrypt)
{
if (ExeFSSizeInMediaUnits > 0)
{
// If we're decrypting, we need to decrypt the filename table first
if (!encrypt)
ProcessExeFSFilenameTable(reader, writer, mediaUnitSize, encrypt);
// For all but the original crypto method, process each of the files in the table
if (Flags.CryptoMethod != CryptoMethod.Original)
{
reader.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
ExeFSHeader exefsHeader = ExeFSHeader.Read(reader);
if (exefsHeader != null)
{
foreach (ExeFSFileHeader fileHeader in exefsHeader.FileHeaders)
{
// Only decrypt a file if it's a code binary
if (!fileHeader.IsCodeBinary)
continue;
uint datalenM = ((fileHeader.FileSize) / (1024 * 1024));
uint datalenB = ((fileHeader.FileSize) % (1024 * 1024));
uint ctroffset = ((fileHeader.FileOffset + mediaUnitSize) / 0x10);
byte[] exefsIVWithOffsetForHeader = Helper.AddToByteArray(ExeFSIV, (int)ctroffset);
var firstCipher = Helper.CreateAESCipher(NormalKey, exefsIVWithOffsetForHeader, encrypt);
var secondCipher = Helper.CreateAESCipher(NormalKey2C, exefsIVWithOffsetForHeader, !encrypt);
reader.BaseStream.Seek((((Entry.Offset + ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin);
writer.BaseStream.Seek((((Entry.Offset + ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin);
if (datalenM > 0)
{
for (int i = 0; i < datalenM; i++)
{
writer.Write(secondCipher.ProcessBytes(firstCipher.ProcessBytes(reader.ReadBytes(1024 * 1024))));
writer.Flush();
Console.Write($"\rPartition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {i} / {datalenM + 1} mb...");
}
}
if (datalenB > 0)
{
writer.Write(secondCipher.DoFinal(firstCipher.DoFinal(reader.ReadBytes((int)datalenB))));
writer.Flush();
}
Console.Write($"\rPartition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {datalenM + 1} / {datalenM + 1} mb... Done!\r\n");
}
}
}
// If we're encrypting, we need to encrypt the filename table now
if (encrypt)
ProcessExeFSFilenameTable(reader, writer, mediaUnitSize, encrypt);
// Process the ExeFS
int exefsSizeM = (int)((ExeFSSizeInMediaUnits - 1) * mediaUnitSize) / (1024 * 1024);
int exefsSizeB = (int)((ExeFSSizeInMediaUnits - 1) * mediaUnitSize) % (1024 * 1024);
int ctroffsetE = (int)(mediaUnitSize / 0x10);
byte[] exefsIVWithOffset = Helper.AddToByteArray(ExeFSIV, ctroffsetE);
var exeFS = Helper.CreateAESCipher(NormalKey2C, exefsIVWithOffset, encrypt);
reader.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits + 1) * mediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits + 1) * mediaUnitSize, SeekOrigin.Begin);
if (exefsSizeM > 0)
{
for (int i = 0; i < exefsSizeM; i++)
{
writer.Write(exeFS.ProcessBytes(reader.ReadBytes(1024 * 1024)));
writer.Flush();
Console.Write($"\rPartition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {i} / {exefsSizeM + 1} mb");
}
}
if (exefsSizeB > 0)
{
writer.Write(exeFS.DoFinal(reader.ReadBytes(exefsSizeB)));
writer.Flush();
}
Console.Write($"\rPartition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {exefsSizeM + 1} / {exefsSizeM + 1} mb... Done!\r\n");
}
else
{
Console.WriteLine($"Partition {PartitionNumber} ExeFS: No Data... Skipping...");
}
}
/// <summary>
/// Process the ExeFS Filename Table
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="mediaUnitSize">Number of bytes per media unit</param>
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
private void ProcessExeFSFilenameTable(BinaryReader reader, BinaryWriter writer, uint mediaUnitSize, bool encrypt)
{
reader.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((Entry.Offset + ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
Console.WriteLine($"Partition {PartitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table");
var exeFSFilenameTable = Helper.CreateAESCipher(NormalKey2C, ExeFSIV, encrypt);
writer.Write(exeFSFilenameTable.ProcessBytes(reader.ReadBytes((int)mediaUnitSize)));
writer.Flush();
}
/// <summary>
/// Process the RomFS, if it exists
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="mediaUnitSize">Number of bytes per media unit</param>
/// <param name="backupFlags">File backup flags for encryption</param>
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
/// <param name="development">True if development keys should be used, false otherwise</param>
private void ProcessRomFS(BinaryReader reader, BinaryWriter writer, uint mediaUnitSize, NCCHHeaderFlags backupFlags, bool encrypt, bool development)
{
if (RomFSOffsetInMediaUnits != 0)
{
int romfsSizeM = (int)(RomFSSizeInMediaUnits * mediaUnitSize) / (1024 * 1024);
int romfsSizeB = (int)(RomFSSizeInMediaUnits * mediaUnitSize) % (1024 * 1024);
// Encrypting RomFS for partitions 1 and up always use Key0x2C
if (encrypt && PartitionNumber > 0)
{
// If the backup flags aren't provided and we're encrypting, assume defaults
if (backupFlags == null)
{
KeyX = KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
NormalKey = Helper.RotateLeft((Helper.RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
if ((backupFlags.BitMasks & BitMasks.FixedCryptoKey) != 0) // except if using zero-key
{
NormalKey = 0x00;
}
else
{
KeyX = KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
NormalKey = Helper.RotateLeft((Helper.RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
}
var cipher = Helper.CreateAESCipher(NormalKey, RomFSIV, encrypt);
reader.BaseStream.Seek((Entry.Offset + RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
writer.BaseStream.Seek((Entry.Offset + RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin);
if (romfsSizeM > 0)
{
for (int i = 0; i < romfsSizeM; i++)
{
writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024)));
writer.Flush();
Console.Write($"\rPartition {PartitionNumber} RomFS: Decrypting: {i} / {romfsSizeM + 1} mb");
}
}
if (romfsSizeB > 0)
{
writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB)));
writer.Flush();
}
Console.Write($"\rPartition {PartitionNumber} RomFS: Decrypting: {romfsSizeM + 1} / {romfsSizeM + 1} mb... Done!\r\n");
}
else
{
Console.WriteLine($"Partition {PartitionNumber} RomFS: No Data... Skipping...");
}
}
/// <summary>
/// Update the CryptoMethod and BitMasks for the partition
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="header">NCSD header for the 3DS file</param>
/// <param name="encrypt">True if we're writing encrypted values, false otherwise</param>
private void UpdateCryptoAndMasks(BinaryReader reader, BinaryWriter writer, NCSDHeader header, bool encrypt)
{
// Write the new CryptoMethod
writer.BaseStream.Seek((Entry.Offset * header.MediaUnitSize) + 0x18B, SeekOrigin.Begin);
if (encrypt)
{
// For partitions 1 and up, set crypto-method to 0x00
if (PartitionNumber > 0)
writer.Write((byte)CryptoMethod.Original);
// If partition 0, restore crypto-method from backup flags
else
writer.Write((byte)header.BackupHeader.Flags.CryptoMethod);
}
else
{
writer.Write((byte)CryptoMethod.Original);
}
writer.Flush();
// Write the new BitMasks flag
writer.BaseStream.Seek((Entry.Offset * header.MediaUnitSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = Flags.BitMasks;
if (encrypt)
{
flag = (flag & ((BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF));
flag = (flag | (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & header.BackupHeader.Flags.BitMasks);
}
else
{
flag = flag & (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag = (flag | BitMasks.NoCrypto);
}
writer.Write((byte)flag);
writer.Flush();
}
}
}

View File

@@ -49,6 +49,11 @@ namespace ThreeDS.Headers
/// </summary>
public BitMasks BitMasks { get; private set; }
/// <summary>
/// Get if the NoCrypto bit is set
/// </summary>
public bool PossblyDecrypted { get { return (BitMasks & BitMasks.NoCrypto) != 0; } }
/// <summary>
/// Read from a stream and get an NCCH header flags, if possible
/// </summary>

View File

@@ -40,9 +40,34 @@ namespace ThreeDS.Headers
/// </summary>
public PartitionTableEntry[] PartitionsTable { get; private set; }
/// <summary>
/// Partition table entry for Executable Content (CXI)
/// </summary>
public PartitionTableEntry ExecutableContent { get { return PartitionsTable[0]; } }
/// <summary>
/// Partition table entry for E-Manual (CFA)
/// </summary>
public PartitionTableEntry EManual { get { return PartitionsTable[1]; } }
/// <summary>
/// Partition table entry for Download Play Child container (CFA)
/// </summary>
public PartitionTableEntry DownloadPlayChildContainer { get { return PartitionsTable[2]; } }
/// <summary>
/// Partition table entry for New3DS Update Data (CFA)
/// </summary>
public PartitionTableEntry New3DSUpdateData { get { return PartitionsTable[6]; } }
/// <summary>
/// Partition table entry for Update Data (CFA)
/// </summary>
public PartitionTableEntry UpdateData { get { return PartitionsTable[7]; } }
#endregion
#region For carts
#region CTR Cart Image (CCI) Specific
/// <summary>
/// Exheader SHA-256 hash
@@ -62,13 +87,38 @@ namespace ThreeDS.Headers
/// <summary>
/// Partition Flags
/// </summary>
private byte[] partitionFlags;
public byte BackupWriteWaitTime { get { return partitionFlags[(int)NCSDFlags.BackupWriteWaitTime]; } }
public MediaCardDeviceType MediaCardDevice3X { get { return (MediaCardDeviceType)partitionFlags[(int)NCSDFlags.MediaCardDevice3X]; } }
public MediaPlatformIndex MediaPlatformIndex { get { return (MediaPlatformIndex)partitionFlags[(int)NCSDFlags.MediaPlatformIndex]; } }
public MediaTypeIndex MediaTypeIndex { get { return (MediaTypeIndex)partitionFlags[(int)NCSDFlags.MediaTypeIndex]; } }
public uint SectorSize { get { return (uint)(0x200 * Math.Pow(2, partitionFlags[(int)NCSDFlags.MediaUnitSize])); } }
public MediaCardDeviceType MediaCardDevice2X { get { return (MediaCardDeviceType)partitionFlags[(int)NCSDFlags.MediaCardDevice2X]; } }
public byte[] PartitionFlags { get; private set; }
/// <summary>
/// Backup Write Wait Time (The time to wait to write save to backup after the card is recognized (0-255
/// seconds)).NATIVE_FIRM loads this flag from the gamecard NCSD header starting with 6.0.0-11.
/// </summary>
public byte BackupWriteWaitTime { get { return PartitionFlags[(int)NCSDFlags.BackupWriteWaitTime]; } }
/// <summary>
/// Media Card Device (1 = NOR Flash, 2 = None, 3 = BT) (SDK 3.X+)
/// </summary>
public MediaCardDeviceType MediaCardDevice3X { get { return (MediaCardDeviceType)PartitionFlags[(int)NCSDFlags.MediaCardDevice3X]; } }
/// <summary>
/// Media Platform Index (1 = CTR)
/// </summary>
public MediaPlatformIndex MediaPlatformIndex { get { return (MediaPlatformIndex)PartitionFlags[(int)NCSDFlags.MediaPlatformIndex]; } }
/// <summary>
/// Media Type Index (0 = Inner Device, 1 = Card1, 2 = Card2, 3 = Extended Device)
/// </summary>
public MediaTypeIndex MediaTypeIndex { get { return (MediaTypeIndex)PartitionFlags[(int)NCSDFlags.MediaTypeIndex]; } }
/// <summary>
/// Media Unit Size i.e. u32 MediaUnitSize = 0x200*2^flags[6];
/// </summary>
public uint MediaUnitSize { get { return (uint)(0x200 * Math.Pow(2, PartitionFlags[(int)NCSDFlags.MediaUnitSize])); } }
/// <summary>
/// Media Card Device (1 = NOR Flash, 2 = None, 3 = BT) (Only SDK 2.X)
/// </summary>
public MediaCardDeviceType MediaCardDevice2X { get { return (MediaCardDeviceType)PartitionFlags[(int)NCSDFlags.MediaCardDevice2X]; } }
/// <summary>
/// Partition ID table
@@ -96,11 +146,11 @@ namespace ThreeDS.Headers
/// <summary>
/// Support for this was implemented with 9.6.0-X FIRM, see below regarding save crypto.
/// </summary>
public byte FIrmUpdateByte2 { get; private set; }
public byte FirmUpdateByte2 { get; private set; }
#endregion
#region For NAND
#region Raw NAND Format Specific
/// <summary>
/// Unknown
@@ -114,12 +164,95 @@ namespace ThreeDS.Headers
#endregion
#region Card Info Header
/// <summary>
/// CARD2: Writable Address In Media Units (For 'On-Chip' Savedata). CARD1: Always 0xFFFFFFFF.
/// </summary>
public byte[] CARD2WritableAddressMediaUnits { get; private set; }
/// <summary>
/// Card Info Bitmask
/// </summary>
public byte[] CardInfoBytemask { get; private set; }
/// <summary>
/// Reserved1
/// </summary>
public byte[] Reserved3 { get; private set; }
/// <summary>
/// Title version
/// </summary>
public ushort TitleVersion { get; private set; }
/// <summary>
/// Card revision
/// </summary>
public ushort CardRevision { get; private set; }
/// <summary>
/// Reserved2
/// </summary>
public byte[] Reserved4 { get; private set; }
/// <summary>
/// Card seed keyY (first u64 is Media ID (same as first NCCH partitionId))
/// </summary>
public byte[] CardSeedKeyY { get; private set; }
/// <summary>
/// Encrypted card seed (AES-CCM, keyslot 0x3B for retail cards, see CTRCARD_SECSEED) /// </summary>
public byte[] EncryptedCardSeed { get; private set; }
/// <summary>
/// Card seed AES-MAC
/// </summary>
public byte[] CardSeedAESMAC { get; private set; }
/// <summary>
/// Card seed nonce
/// </summary>
public byte[] CardSeedNonce { get; private set; }
/// <summary>
/// Reserved3
/// </summary>
public byte[] Reserved5 { get; private set; }
/// <summary>
/// Copy of first NCCH header (excluding RSA signature)
/// </summary>
public NCCHHeader BackupHeader { get; private set; }
#endregion
#region Development Card Info Header Extension
/// <summary>
/// CardDeviceReserved1
/// </summary>
public byte[] CardDeviceReserved1 { get; private set; }
/// <summary>
/// TitleKey
/// </summary>
public byte[] TitleKey { get; private set; }
/// <summary>
/// CardDeviceReserved2
/// </summary>
public byte[] CardDeviceReserved2 { get; private set; }
#endregion
/// <summary>
/// Read from a stream and get an NCSD header, if possible
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="development">True if development cart, false otherwise</param>
/// <returns>NCSD header object, null on error</returns>
public static NCSDHeader Read(BinaryReader reader)
public static NCSDHeader Read(BinaryReader reader, bool development)
{
NCSDHeader header = new NCSDHeader();
@@ -145,7 +278,7 @@ namespace ThreeDS.Headers
header.ExheaderHash = reader.ReadBytes(0x20);
header.AdditionalHeaderSize = reader.ReadUInt32();
header.SectorZeroOffset = reader.ReadUInt32();
header.partitionFlags = reader.ReadBytes(8);
header.PartitionFlags = reader.ReadBytes(8);
header.PartitionIdTable = new byte[8][];
for (int i = 0; i < 8; i++)
@@ -154,7 +287,27 @@ namespace ThreeDS.Headers
header.Reserved1 = reader.ReadBytes(0x20);
header.Reserved2 = reader.ReadBytes(0xE);
header.FirmUpdateByte1 = reader.ReadByte();
header.FIrmUpdateByte2 = reader.ReadByte();
header.FirmUpdateByte2 = reader.ReadByte();
header.CARD2WritableAddressMediaUnits = reader.ReadBytes(4);
header.CardInfoBytemask = reader.ReadBytes(4);
header.Reserved3 = reader.ReadBytes(0x108);
header.TitleVersion = reader.ReadUInt16();
header.CardRevision = reader.ReadUInt16();
header.Reserved4 = reader.ReadBytes(0xCEC); // Incorrectly documented as 0xCEE
header.CardSeedKeyY = reader.ReadBytes(0x10);
header.EncryptedCardSeed = reader.ReadBytes(0x10);
header.CardSeedAESMAC = reader.ReadBytes(0x10);
header.CardSeedNonce = reader.ReadBytes(0xC);
header.Reserved5 = reader.ReadBytes(0xC4);
header.BackupHeader = NCCHHeader.Read(reader, false);
if (development)
{
header.CardDeviceReserved1 = reader.ReadBytes(0x200);
header.TitleKey = reader.ReadBytes(0x10);
header.CardDeviceReserved2 = reader.ReadBytes(0xF0);
}
}
else if (header.PartitionsFSType == FilesystemType.FIRM)
{
@@ -169,5 +322,54 @@ namespace ThreeDS.Headers
return null;
}
}
/// <summary>
/// Process all partitions in the partition table
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="encrypt">True if we want to encrypt the partitions, false otherwise</param>
/// <param name="development">True if development keys should be used, false otherwise</param>
public void ProcessAllPartitions(BinaryReader reader, BinaryWriter writer, bool encrypt, bool development)
{
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
NCCHHeader partitionHeader = GetPartitionHeader(reader, p);
if (partitionHeader == null)
continue;
partitionHeader.ProcessPartition(reader, writer, this, encrypt, development);
}
}
/// <summary>
/// Get a specific partition header from the partition table
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="partitionNumber">Partition number to attempt to retrieve</param>
/// <returns>NCCH header for the partition requested, null on error</returns>
public NCCHHeader GetPartitionHeader(BinaryReader reader, int partitionNumber)
{
if (!PartitionsTable[partitionNumber].IsValid())
{
Console.WriteLine($"Partition {partitionNumber} Not found... Skipping...");
return null;
}
// Seek to the beginning of the NCCH partition
reader.BaseStream.Seek((PartitionsTable[partitionNumber].Offset * MediaUnitSize), SeekOrigin.Begin);
NCCHHeader partitionHeader = NCCHHeader.Read(reader, true);
if (partitionHeader == null)
{
Console.WriteLine($"Partition {partitionNumber} Unable to read NCCH header");
return null;
}
partitionHeader.PartitionNumber = partitionNumber;
partitionHeader.Entry = PartitionsTable[partitionNumber];
return partitionHeader;
}
}
}

86
3DSDecrypt/Helper.cs Normal file
View File

@@ -0,0 +1,86 @@
using System;
using System.Linq;
using System.Numerics;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
namespace ThreeDS
{
public static class Helper
{
/// <summary>
/// Add an integer value to a number represented by a byte array
/// </summary>
/// <param name="input">Byte array to add to</param>
/// <param name="add">Amount to add</param>
/// <returns>Byte array representing the new value</returns>
public static byte[] AddToByteArray(byte[] input, int add)
{
int len = input.Length;
var bigint = new BigInteger(input.Reverse().ToArray());
bigint += add;
var arr = bigint.ToByteArray().Reverse().ToArray();
if (arr.Length < len)
{
byte[] temp = new byte[len];
for (int i = 0; i < (len - arr.Length); i++)
temp[i] = 0x00;
Array.Copy(arr, 0, temp, len - arr.Length, arr.Length);
arr = temp;
}
return arr;
}
/// <summary>
/// Create AES cipher and intialize
/// </summary>
/// <param name="key">BigInteger representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <param name="encrypt">True if cipher is created for encryption, false otherwise</param>
/// <returns>Initialized AES cipher</returns>
public static IBufferedCipher CreateAESCipher(BigInteger key, byte[] iv, bool encrypt)
{
var cipher = CipherUtilities.GetCipher("AES/CTR");
cipher.Init(encrypt, new ParametersWithIV(new KeyParameter(TakeSixteen(key)), iv));
return cipher;
}
/// <summary>
/// Perform a rotate left on a BigInteger
/// </summary>
/// <param name="val">BigInteger value to rotate</param>
/// <param name="r_bits">Number of bits to rotate</param>
/// <param name="max_bits">Maximum number of bits to rotate on</param>
/// <returns>Rotated BigInteger value</returns>
public static BigInteger RotateLeft(BigInteger val, int r_bits, int max_bits)
{
return (val << r_bits % max_bits) & (BigInteger.Pow(2, max_bits) - 1) | ((val & (BigInteger.Pow(2, max_bits) - 1)) >> (max_bits - (r_bits % max_bits)));
}
/// <summary>
/// Get a 16-byte array representation of a BigInteger
/// </summary>
/// <param name="input">BigInteger value to convert</param>
/// <returns>16-byte array representing the BigInteger</returns>
private static byte[] TakeSixteen(BigInteger input)
{
var arr = input.ToByteArray().Take(16).Reverse().ToArray();
if (arr.Length < 16)
{
byte[] temp = new byte[16];
for (int i = 0; i < (16 - arr.Length); i++)
temp[i] = 0x00;
Array.Copy(arr, 0, temp, 16 - arr.Length, arr.Length);
arr = temp;
}
return arr;
}
}
}

View File

@@ -7,9 +7,24 @@ namespace ThreeDS
{
public static void Main(string[] args)
{
if (args.Length < 2 || (args[0] != "encrypt" && args[0] != "decrypt"))
if (args.Length < 2)
{
Console.WriteLine("Usage: 3dsdecrypt.exe (decrypt|encrypt) [-dev] <file|dir> ...");
DisplayHelp("Not enough arguments");
return;
}
bool? encrypt = null;
if (args[0] == "decrypt")
{
encrypt = false;
}
else if (args[0] == "encrypt")
{
encrypt = true;
}
else
{
DisplayHelp($"Invalid operation: {args[0]}");
return;
}
@@ -25,25 +40,31 @@ namespace ThreeDS
{
if (File.Exists(args[i]))
{
ThreeDSTool tool = new ThreeDSTool(args[i], development);
if (args[0] == "decrypt")
tool.Decrypt();
else if (args[0] == "encrypt")
tool.Encrypt();
ThreeDSTool tool = new ThreeDSTool(args[i], development, encrypt.Value);
if (!tool.ProcessFile())
Console.WriteLine("Processing failed!");
}
else if (Directory.Exists(args[i]))
{
foreach (string file in Directory.EnumerateFiles(args[i], "*", SearchOption.AllDirectories))
{
ThreeDSTool tool = new ThreeDSTool(file, development);
if (args[0] == "decrypt")
tool.Decrypt();
else if (args[0] == "encrypt")
tool.Encrypt();
ThreeDSTool tool = new ThreeDSTool(file, development, encrypt.Value);
if (!tool.ProcessFile())
Console.WriteLine("Processing failed!");
}
}
}
Console.WriteLine("Press Enter to Exit...");
Console.Read();
}
private static void DisplayHelp(string err = null)
{
if (!string.IsNullOrWhiteSpace(err))
Console.WriteLine($"Error: {err}");
Console.WriteLine("Usage: 3dsdecrypt.exe (decrypt|encrypt) [-dev] <file|dir> ...");
}
}
}

View File

@@ -1,11 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Numerics;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using ThreeDS.Data;
using ThreeDS.Headers;
namespace ThreeDS
@@ -23,535 +17,44 @@ namespace ThreeDS
private readonly bool development;
/// <summary>
/// Boot rom key
/// Flag to determine if encrypting or decrypting
/// </summary>
private BigInteger KeyX;
private readonly bool encrypt;
/// <summary>
/// NCCH boot rom key
/// </summary>
private BigInteger KeyX2C;
/// <summary>
/// Kernel9/Process9 key
/// </summary>
private BigInteger KeyY;
/// <summary>
/// Normal AES key
/// </summary>
private BigInteger NormalKey;
/// <summary>
/// NCCH AES key
/// </summary>
private BigInteger NormalKey2C;
public ThreeDSTool(string filename, bool development)
public ThreeDSTool(string filename, bool development, bool encrypt)
{
this.filename = filename;
this.development = development;
this.encrypt = encrypt;
}
/// <summary>
/// Attempt to decrypt a 3DS file
/// Process an input file given the input values
/// </summary>
public void Decrypt()
public bool ProcessFile()
{
if (!File.Exists(filename))
return;
// Make sure we have a file to process first
Console.WriteLine(filename);
if (!File.Exists(filename))
return false;
using (BinaryReader f = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter g = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
// Open the read and write on the same file for inplace processing
using (BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
{
NCSDHeader header = NCSDHeader.Read(f);
NCSDHeader header = NCSDHeader.Read(reader, development);
if (header == null)
{
Console.WriteLine("Error: Not a 3DS Rom!");
return;
return false;
}
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
if (!header.PartitionsTable[p].IsValid())
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Seek to the beginning of the NCCH partition
f.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize), SeekOrigin.Begin);
NCCHHeader partitionHeader = NCCHHeader.Read(f);
if (partitionHeader == null)
{
Console.WriteLine($"Partition {p} Unable to read NCCH header");
continue;
}
// Check if the 'NoCrypto' bit is set
if ((partitionHeader.Flags.BitMasks & BitMasks.NoCrypto) != 0)
{
Console.WriteLine($"Partition {p}: Already Decrypted?...");
continue;
}
// Determine the Keys to be used
SetEncryptionKeys(partitionHeader.RSA2048Signature, partitionHeader.Flags.BitMasks, partitionHeader.Flags.CryptoMethod, p);
// Decrypt each of the pieces if they exist
ProcessExtendedHeader(f, g, header, p, partitionHeader, false);
ProcessExeFS(f, g, header, p, partitionHeader, false);
ProcessRomFS(f, g, header, p, partitionHeader, false);
// Write the new CryptoMethod
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x18B, SeekOrigin.Begin);
g.Write((byte)CryptoMethod.Original);
g.Flush();
// Write the new BitMasks flag
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = partitionHeader.Flags.BitMasks;
flag = flag & (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF);
flag = (flag | BitMasks.NoCrypto);
g.Write((byte)flag);
g.Flush();
}
Console.WriteLine("Press Enter to Exit...");
Console.Read();
}
}
/// <summary>
/// Attempt to encrypt a 3DS file
/// </summary>
public void Encrypt()
{
if (!File.Exists(filename))
return;
Console.WriteLine(filename);
using (BinaryReader f = new BinaryReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using (BinaryWriter g = new BinaryWriter(File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
{
NCSDHeader header = NCSDHeader.Read(f);
if (header == null)
{
Console.WriteLine("Error: Not a 3DS Rom!");
return;
}
// Get the backup flags
f.BaseStream.Seek(0x1188, SeekOrigin.Begin);
NCCHHeaderFlags backupFlags = NCCHHeaderFlags.Read(f);
// Iterate over all 8 NCCH partitions
for (int p = 0; p < 8; p++)
{
if (!header.PartitionsTable[p].IsValid())
{
Console.WriteLine($"Partition {p} Not found... Skipping...");
continue;
}
// Seek to the beginning of the NCCH partition
f.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize), SeekOrigin.Begin);
NCCHHeader partitionHeader = NCCHHeader.Read(f);
if (partitionHeader == null)
{
Console.WriteLine($"Partition {p} Unable to read NCCH header");
continue;
}
// Check if the 'NoCrypto' bit is not set
if ((partitionHeader.Flags.BitMasks & BitMasks.NoCrypto) == 0)
{
Console.WriteLine($"Partition {p}: Already Encrypted?...");
continue;
}
// Determine the Keys to be used
SetEncryptionKeys(partitionHeader.RSA2048Signature, backupFlags.BitMasks, backupFlags.CryptoMethod, p);
// Encrypt each of the pieces if they exist
ProcessExtendedHeader(f, g, header, p, partitionHeader, true);
ProcessExeFS(f, g, header, p, partitionHeader, true);
ProcessRomFS(f, g, header, p, partitionHeader, true, backupFlags);
// Write the new CryptoMethod
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x18B, SeekOrigin.Begin);
if (p > 0)
{
g.Write((byte)CryptoMethod.Original); // For partitions 1 and up, set crypto-method to 0x00
g.Flush();
}
else
{
g.Write((byte)backupFlags.CryptoMethod); // If partition 0, restore crypto-method from backup flags
g.Flush();
}
// Write the new BitMasks flag
g.BaseStream.Seek((header.PartitionsTable[p].Offset * header.SectorSize) + 0x18F, SeekOrigin.Begin);
BitMasks flag = partitionHeader.Flags.BitMasks;
flag = (flag & ((BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF));
flag = (flag | (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & backupFlags.BitMasks);
g.Write((byte)flag);
g.Flush();
}
Console.WriteLine("Press Enter to Exit...");
Console.Read();
}
}
/// <summary>
/// Perform a rotate left on a BigInteger
/// </summary>
/// <param name="val">BigInteger value to rotate</param>
/// <param name="r_bits">Number of bits to rotate</param>
/// <param name="max_bits">Maximum number of bits to rotate on</param>
/// <returns>Rotated BigInteger value</returns>
private BigInteger RotateLeft(BigInteger val, int r_bits, int max_bits)
{
return (val << r_bits % max_bits) & (BigInteger.Pow(2, max_bits) - 1) | ((val & (BigInteger.Pow(2, max_bits) - 1)) >> (max_bits - (r_bits % max_bits)));
}
/// <summary>
/// Determine the set of keys to be used for encryption or decryption
/// </summary>
/// <param name="rsaSignature">RSA-2048 signature from a partition header</param>
/// <param name="masks">BitMasks value for a partition header or backup header</param>
/// <param name="method">CryptoMethod used for the partition</param>
/// <param name="partitionNumber">Partition number, only used for logging</param>
private void SetEncryptionKeys(byte[] rsaSignature, BitMasks masks, CryptoMethod method, int partitionNumber)
{
KeyX = 0;
KeyX2C = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
KeyY = new BigInteger(rsaSignature.Take(16).Reverse().ToArray()); // KeyY is the first 16 bytes of the partition RSA-2048 SHA-256 signature
NormalKey = 0;
NormalKey2C = RotateLeft((RotateLeft(KeyX2C, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
if ((masks & BitMasks.FixedCryptoKey) != 0)
{
NormalKey = 0x00;
NormalKey2C = 0x00;
if (partitionNumber == 0)
Console.WriteLine("Encryption Method: Zero Key");
}
else
{
if (method == CryptoMethod.Original)
{
KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
if (partitionNumber == 0)
Console.WriteLine("Encryption Method: Key 0x2C");
}
else if (method == CryptoMethod.Seven)
{
KeyX = (development ? Constants.KeyX0x25 : Constants.KeyX0x25);
if (partitionNumber == 0)
Console.WriteLine("Encryption Method: Key 0x25");
}
else if (method == CryptoMethod.NineThree)
{
KeyX = (development ? Constants.DevKeyX0x18 : Constants.KeyX0x18);
if (partitionNumber == 0)
Console.WriteLine("Encryption Method: Key 0x18");
}
else if (method == CryptoMethod.NineSix)
{
KeyX = (development ? Constants.DevKeyX0x1B : Constants.KeyX0x1B);
if (partitionNumber == 0)
Console.WriteLine("Encryption Method: Key 0x1B");
}
NormalKey = RotateLeft((RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
}
/// <summary>
/// Add an integer value to a number represented by a byte array
/// </summary>
/// <param name="input">Byte array to add to</param>
/// <param name="add">Amount to add</param>
/// <returns>Byte array representing the new value</returns>
private byte[] AddToByteArray(byte[] input, int add)
{
int len = input.Length;
var bigint = new BigInteger(input.Reverse().ToArray());
bigint += add;
var arr = bigint.ToByteArray().Reverse().ToArray();
if (arr.Length < len)
{
byte[] temp = new byte[len];
for (int i = 0; i < (len - arr.Length); i++)
temp[i] = 0x00;
Array.Copy(arr, 0, temp, len - arr.Length, arr.Length);
arr = temp;
// Process all 8 NCCH partitions
header.ProcessAllPartitions(reader, writer, encrypt, development);
}
return arr;
}
/// <summary>
/// Create AES cipher and intialize
/// </summary>
/// <param name="key">BigInteger representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <param name="encrypt">True if cipher is created for encryption, false otherwise</param>
/// <returns>Initialized AES cipher</returns>
private IBufferedCipher CreateAESCipher(BigInteger key, byte[] iv, bool encrypt)
{
var cipher = CipherUtilities.GetCipher("AES/CTR");
cipher.Init(encrypt, new ParametersWithIV(new KeyParameter(TakeSixteen(key)), iv));
return cipher;
}
/// <summary>
/// Get a 16-byte array representation of a BigInteger
/// </summary>
/// <param name="input">BigInteger value to convert</param>
/// <returns>16-byte array representing the BigInteger</returns>
private byte[] TakeSixteen(BigInteger input)
{
var arr = input.ToByteArray().Take(16).Reverse().ToArray();
if (arr.Length < 16)
{
byte[] temp = new byte[16];
for (int i = 0; i < (16 - arr.Length); i++)
temp[i] = 0x00;
Array.Copy(arr, 0, temp, 16 - arr.Length, arr.Length);
arr = temp;
}
return arr;
}
/// <summary>
/// Process the extended header, if it exists
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="header">File header</param>
/// <param name="partitionNumber">Partition number for logging</param>
/// <param name="partitionHeader">Partition header</param>
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
private void ProcessExtendedHeader(BinaryReader reader, BinaryWriter writer, NCSDHeader header, int partitionNumber, NCCHHeader partitionHeader, bool encrypt)
{
if (partitionHeader.ExtendedHeaderSizeInBytes > 0)
{
reader.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset * header.SectorSize) + 0x200, SeekOrigin.Begin);
writer.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset * header.SectorSize) + 0x200, SeekOrigin.Begin);
Console.WriteLine($"Partition {partitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + ": ExHeader");
var cipher = CreateAESCipher(NormalKey2C, partitionHeader.PlainIV, encrypt);
writer.Write(cipher.ProcessBytes(reader.ReadBytes(Constants.CXTExtendedDataHeaderLength)));
writer.Flush();
}
else
{
Console.WriteLine($"Partition {partitionNumber} ExeFS: No Extended Header... Skipping...");
}
}
/// <summary>
/// Process the ExeFS, if it exists
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="header">File header</param>
/// <param name="partitionNumber">Partition number for logging</param>
/// <param name="partitionHeader">Partition header</param>
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
private void ProcessExeFS(BinaryReader reader, BinaryWriter writer, NCSDHeader header, int partitionNumber, NCCHHeader partitionHeader, bool encrypt)
{
if (partitionHeader.ExeFSSizeInMediaUnits > 0)
{
// If we're decrypting, we need to decrypt the filename table first
if (!encrypt)
ProcessExeFSFilenameTable(reader, writer, header, partitionNumber, partitionHeader, encrypt);
// For all but the original crypto method, process each of the files in the table
if (partitionHeader.Flags.CryptoMethod != CryptoMethod.Original)
{
reader.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
ExeFSHeader exefsHeader = ExeFSHeader.Read(reader);
if (exefsHeader != null)
{
foreach (ExeFSFileHeader fileHeader in exefsHeader.FileHeaders)
{
// Only decrypt a file if it's a code binary
if (!fileHeader.IsCodeBinary)
continue;
uint datalenM = ((fileHeader.FileSize) / (1024 * 1024));
uint datalenB = ((fileHeader.FileSize) % (1024 * 1024));
uint ctroffset = ((fileHeader.FileOffset + header.SectorSize) / 0x10);
byte[] exefsIVWithOffsetForHeader = AddToByteArray(partitionHeader.ExeFSIV, (int)ctroffset);
var firstCipher = CreateAESCipher(NormalKey, exefsIVWithOffsetForHeader, encrypt);
var secondCipher = CreateAESCipher(NormalKey2C, exefsIVWithOffsetForHeader, !encrypt);
reader.BaseStream.Seek((((header.PartitionsTable[partitionNumber].Offset + partitionHeader.ExeFSOffsetInMediaUnits) + 1) * header.SectorSize) + fileHeader.FileOffset, SeekOrigin.Begin);
writer.BaseStream.Seek((((header.PartitionsTable[partitionNumber].Offset + partitionHeader.ExeFSOffsetInMediaUnits) + 1) * header.SectorSize) + fileHeader.FileOffset, SeekOrigin.Begin);
if (datalenM > 0)
{
for (int i = 0; i < datalenM; i++)
{
writer.Write(secondCipher.ProcessBytes(firstCipher.ProcessBytes(reader.ReadBytes(1024 * 1024))));
writer.Flush();
Console.Write($"\rPartition {partitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {i} / {datalenM + 1} mb...");
}
}
if (datalenB > 0)
{
writer.Write(secondCipher.DoFinal(firstCipher.DoFinal(reader.ReadBytes((int)datalenB))));
writer.Flush();
}
Console.Write($"\rPartition {partitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {datalenM + 1} / {datalenM + 1} mb... Done!\r\n");
}
}
}
// If we're encrypting, we need to encrypt the filename table now
if (encrypt)
ProcessExeFSFilenameTable(reader, writer, header, partitionNumber, partitionHeader, encrypt);
// Process the ExeFS
int exefsSizeM = (int)((partitionHeader.ExeFSSizeInMediaUnits - 1) * header.SectorSize) / (1024 * 1024);
int exefsSizeB = (int)((partitionHeader.ExeFSSizeInMediaUnits - 1) * header.SectorSize) % (1024 * 1024);
int ctroffsetE = (int)(header.SectorSize / 0x10);
byte[] exefsIVWithOffset = AddToByteArray(partitionHeader.ExeFSIV, ctroffsetE);
var exeFS = CreateAESCipher(NormalKey2C, exefsIVWithOffset, encrypt);
reader.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset + partitionHeader.ExeFSOffsetInMediaUnits + 1) * header.SectorSize, SeekOrigin.Begin);
writer.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset + partitionHeader.ExeFSOffsetInMediaUnits + 1) * header.SectorSize, SeekOrigin.Begin);
if (exefsSizeM > 0)
{
for (int i = 0; i < exefsSizeM; i++)
{
writer.Write(exeFS.ProcessBytes(reader.ReadBytes(1024 * 1024)));
writer.Flush();
Console.Write($"\rPartition {partitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {i} / {exefsSizeM + 1} mb");
}
}
if (exefsSizeB > 0)
{
writer.Write(exeFS.DoFinal(reader.ReadBytes(exefsSizeB)));
writer.Flush();
}
Console.Write($"\rPartition {partitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": {exefsSizeM + 1} / {exefsSizeM + 1} mb... Done!\r\n");
}
else
{
Console.WriteLine($"Partition {partitionNumber} ExeFS: No Data... Skipping...");
}
}
/// <summary>
/// Process the ExeFS Filename Table
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="header">File header</param>
/// <param name="partitionNumber">Partition number for logging</param>
/// <param name="partitionHeader">Partition header</param>
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
private void ProcessExeFSFilenameTable(BinaryReader reader, BinaryWriter writer, NCSDHeader header, int partitionNumber, NCCHHeader partitionHeader, bool encrypt)
{
reader.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
writer.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset + partitionHeader.ExeFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
Console.WriteLine($"Partition {partitionNumber} ExeFS: " + (encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table");
var exeFSFilenameTable = CreateAESCipher(NormalKey2C, partitionHeader.ExeFSIV, encrypt);
writer.Write(exeFSFilenameTable.ProcessBytes(reader.ReadBytes((int)header.SectorSize)));
writer.Flush();
}
/// <summary>
/// Process the RomFS, if it exists
/// </summary>
/// <param name="reader">BinaryReader representing the input stream</param>
/// <param name="writer">BinaryWriter representing the output stream</param>
/// <param name="header">File header</param>
/// <param name="partitionNumber">Partition number for logging</param>
/// <param name="partitionHeader">Partition header</param>
/// <param name="encrypt">True if we want to encrypt the extended header, false otherwise</param>
/// <param name="backupFlags">Optional backup flags, only used for encrypt</param>
private void ProcessRomFS(BinaryReader reader, BinaryWriter writer, NCSDHeader header, int partitionNumber, NCCHHeader partitionHeader, bool encrypt, NCCHHeaderFlags backupFlags = null)
{
if (partitionHeader.RomFSOffsetInMediaUnits != 0)
{
int romfsSizeM = (int)(partitionHeader.RomFSSizeInMediaUnits * header.SectorSize) / (1024 * 1024);
int romfsSizeB = (int)(partitionHeader.RomFSSizeInMediaUnits * header.SectorSize) % (1024 * 1024);
// Encrypting RomFS for partitions 1 and up always use Key0x2C
if (encrypt && partitionNumber > 0)
{
// If the backup flags aren't provided and we're encrypting, assume defaults
if (backupFlags == null)
{
KeyX = KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
NormalKey = RotateLeft((RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
if ((backupFlags.BitMasks & BitMasks.FixedCryptoKey) != 0) // except if using zero-key
{
NormalKey = 0x00;
}
else
{
KeyX = KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C);
NormalKey = RotateLeft((RotateLeft(KeyX, 2, 128) ^ KeyY) + Constants.AESHardwareConstant, 87, 128);
}
}
var cipher = CreateAESCipher(NormalKey, partitionHeader.RomFSIV, encrypt);
reader.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset + partitionHeader.RomFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
writer.BaseStream.Seek((header.PartitionsTable[partitionNumber].Offset + partitionHeader.RomFSOffsetInMediaUnits) * header.SectorSize, SeekOrigin.Begin);
if (romfsSizeM > 0)
{
for (int i = 0; i < romfsSizeM; i++)
{
writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024)));
writer.Flush();
Console.Write($"\rPartition {partitionNumber} RomFS: Decrypting: {i} / {romfsSizeM + 1} mb");
}
}
if (romfsSizeB > 0)
{
writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB)));
writer.Flush();
}
Console.Write($"\rPartition {partitionNumber} RomFS: Decrypting: {romfsSizeM + 1} / {romfsSizeM + 1} mb... Done!\r\n");
}
else
{
Console.WriteLine($"Partition {partitionNumber} RomFS: No Data... Skipping...");
}
}
return true;
}
}
}