diff --git a/lib/libnx/include/nx/NcaHeader.h b/lib/libnx/include/nx/NcaHeader.h index e40f813..3b417d4 100644 --- a/lib/libnx/include/nx/NcaHeader.h +++ b/lib/libnx/include/nx/NcaHeader.h @@ -83,6 +83,7 @@ namespace nx void setContentIndex(uint32_t index); uint32_t getSdkAddonVersion() const; void setSdkAddonVersion(uint32_t version); + bool hasRightsId() const; const byte_t* getRightsId() const; void setRightsId(const byte_t* rights_id); const fnd::List& getPartitions() const; diff --git a/lib/libnx/include/nx/NcaUtils.h b/lib/libnx/include/nx/NcaUtils.h index 81f7163..50365b4 100644 --- a/lib/libnx/include/nx/NcaUtils.h +++ b/lib/libnx/include/nx/NcaUtils.h @@ -9,5 +9,6 @@ namespace nx static inline size_t sectorToOffset(size_t sector_index) { return sector_index * nx::nca::kSectorSize; } static void decryptNcaHeader(const byte_t* src, byte_t* dst, const crypto::aes::sAesXts128Key& key); static byte_t getMasterKeyRevisionFromKeyGeneration(byte_t key_generation); + static void getNcaPartitionAesCtr(const nx::sNcaFsHeader* hdr, byte_t* ctr); }; } \ No newline at end of file diff --git a/lib/libnx/include/nx/hierarchicalsha256.h b/lib/libnx/include/nx/hierarchicalsha256.h new file mode 100644 index 0000000..7039dcc --- /dev/null +++ b/lib/libnx/include/nx/hierarchicalsha256.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#include + +namespace nx +{ + namespace hierarchicalsha256 + { + static const size_t kDefaultLevelNum = 2; + } + +#pragma pack(push,1) + struct sHierarchicalSha256Header + { + crypto::sha::sSha256Hash master_hash; + le_uint32_t hash_block_size; + le_uint32_t hash_level_num; + struct sLayout + { + le_uint64_t offset; + le_uint64_t size; + } hash_data, hash_target; + }; +#pragma pack(pop) +} diff --git a/lib/libnx/include/nx/ivfc.h b/lib/libnx/include/nx/ivfc.h index 475f833..0d74426 100644 --- a/lib/libnx/include/nx/ivfc.h +++ b/lib/libnx/include/nx/ivfc.h @@ -11,7 +11,7 @@ namespace nx namespace ivfc { const std::string kIvfcSig = "IVFC"; - static const size_t kMaxIvfcLevel = 4; + static const size_t kMaxIvfcLevel = 7; static const uint32_t kIvfcId = 0x20000; } @@ -24,13 +24,13 @@ namespace nx le_uint32_t level_num; struct sIvfcLevelHeader { - uint64_t logical_offset; - uint64_t hash_data_size; - uint32_t block_size; + le_uint64_t logical_offset; + le_uint64_t hash_data_size; + le_uint32_t block_size; byte_t reserved[4]; } level_header[ivfc::kMaxIvfcLevel]; - byte_t unk_0xA0[0x20]; - byte_t master_hash[0x20]; + byte_t reserved_00[0x8]; + crypto::sha::sSha256Hash master_hash; }; #pragma pack(pop) } diff --git a/lib/libnx/include/nx/nca.h b/lib/libnx/include/nx/nca.h index 04f9036..e301ae7 100644 --- a/lib/libnx/include/nx/nca.h +++ b/lib/libnx/include/nx/nca.h @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include namespace nx { @@ -19,6 +21,8 @@ namespace nx static const size_t kAesKeyNum = 16; static const size_t kRightsIdLen = 0x10; static const size_t kKeyAreaEncryptionKeyNum = 3; + static const size_t kFsHeaderHashSuperblockLen = 0x130; + static const uint16_t kDefaultFsHeaderVersion = 2; enum ProgramPartitionId { @@ -40,6 +44,7 @@ namespace nx TYPE_CONTROL, TYPE_MANUAL, TYPE_DATA, + TYPE_PUBLIC_DATA }; enum KeyBankIndex @@ -67,7 +72,7 @@ namespace nx enum HashType { HASH_AUTO, - HASH_UNK1, + HASH_NONE, HASH_HIERARCHICAL_SHA256, HASH_HIERARCHICAL_INTERGRITY // IVFC }; @@ -78,7 +83,7 @@ namespace nx CRYPT_NONE, CRYPT_AESXTS, CRYPT_AESCTR, - CRYPT_BKTR + CRYPT_AESCTREX }; } @@ -88,7 +93,7 @@ namespace nx char signature[4]; byte_t distribution_type; byte_t content_type; - byte_t key_generation; // KeyGeneration + byte_t key_generation; byte_t key_area_encryption_key_index; le_uint64_t content_size; le_uint64_t program_id; @@ -110,23 +115,26 @@ namespace nx struct sNcaFsHeader { - le_uint16_t version; // usually 0x0002 - byte_t format_type; // RomFs(0x00), PartitionFs(0x01) - byte_t hash_type; // HashTypeAuto(0x00), HashTypeHierarchicalSha256(0x02), HashTypeHierarchicalIntegrity(0x03).RomFs uses (0x03) this is forced, PartitionFs uses (0x02). - byte_t encryption_type; // EncryptionTypeAuto(0x00), EncryptionTypeNone(0x01), EncryptionTypeAesCtr(0x03) - byte_t reserved[3]; + le_uint16_t version; + byte_t format_type; + byte_t hash_type; + byte_t encryption_type; + byte_t reserved_0[3]; + union { + byte_t hash_superblock[nca::kFsHeaderHashSuperblockLen]; + nx::sHierarchicalSha256Header hierarchicalsha256_header; + nx::sIvfcHeader ivfc_header; + }; + crypto::aes::sAesIvCtr base_ctr; + byte_t reserved_1[0xB8]; }; - struct sHierarchicalSha256Header + struct sNcaHeaderBlock { - byte_t master_hash[0x20]; - le_uint32_t hash_block_size; - le_uint32_t unk_0x02; - struct sLayout - { - le_uint64_t offset; - le_uint64_t size; - } hash_data, hash_target; + byte_t signature_main[crypto::rsa::kRsa2048Size]; + byte_t signature_acid[crypto::rsa::kRsa2048Size]; + sNcaHeader header; + sNcaFsHeader fs_header[nx::nca::kPartitionNum]; }; #pragma pack(pop) diff --git a/lib/libnx/source/NcaHeader.cpp b/lib/libnx/source/NcaHeader.cpp index 9ceadb0..34e5d88 100644 --- a/lib/libnx/source/NcaHeader.cpp +++ b/lib/libnx/source/NcaHeader.cpp @@ -219,6 +219,19 @@ void nx::NcaHeader::setSdkAddonVersion(uint32_t version) mSdkAddonVersion = version; } +bool nx::NcaHeader::hasRightsId() const +{ + bool rightsIdIsSet = false; + + for (size_t i = 0; i < nca::kRightsIdLen; i++) + { + if (mRightsId[i] != 0) + rightsIdIsSet = true; + } + + return rightsIdIsSet; +} + const byte_t* nx::NcaHeader::getRightsId() const { return mRightsId; diff --git a/lib/libnx/source/NcaUtils.cpp b/lib/libnx/source/NcaUtils.cpp index 145ccdf..0ffa1ad 100644 --- a/lib/libnx/source/NcaUtils.cpp +++ b/lib/libnx/source/NcaUtils.cpp @@ -46,4 +46,12 @@ byte_t nx::NcaUtils::getMasterKeyRevisionFromKeyGeneration(byte_t key_generation } return masterkey_rev; +} + +void nx::NcaUtils::getNcaPartitionAesCtr(const nx::sNcaFsHeader* hdr, byte_t* ctr) +{ + for (size_t i = 0; i < 16; i++) + { + ctr[15-i] = hdr->base_ctr.iv[i]; + } } \ No newline at end of file diff --git a/programs/nstool/source/AesCtrWrappedIFile.cpp b/programs/nstool/source/AesCtrWrappedIFile.cpp new file mode 100644 index 0000000..29c4344 --- /dev/null +++ b/programs/nstool/source/AesCtrWrappedIFile.cpp @@ -0,0 +1,71 @@ +#include "AesCtrWrappedIFile.h" + +AesCtrWrappedIFile::AesCtrWrappedIFile(fnd::IFile& file, const crypto::aes::sAes128Key& key, const crypto::aes::sAesIvCtr& ctr) : + mFile(file), + mKey(key), + mBaseCtr(ctr) +{ + mScratch.alloc(kAesCtrScratchAllocSize); +} + +size_t AesCtrWrappedIFile::size() +{ + return mFile.size(); +} + +void AesCtrWrappedIFile::seek(size_t offset) +{ + mFile.seek(offset); + crypto::aes::AesIncrementCounter(mBaseCtr.iv, offset>>4, mCurrentCtr.iv); + mBlockOffset = offset & 0xf; +} + +void AesCtrWrappedIFile::read(byte_t* out, size_t len) +{ + for (size_t i = 0; i < (len / kAesCtrScratchSize); i++) + { + mFile.read(mScratch.getBytes() + mBlockOffset, kAesCtrScratchSize); + crypto::aes::AesCtr(mScratch.getBytes(), kAesCtrScratchAllocSize, mKey.key, mCurrentCtr.iv, mScratch.getBytes()); + memcpy(out + (i * kAesCtrScratchSize), mScratch.getBytes() + mBlockOffset, kAesCtrScratchSize); + } + + if (len % kAesCtrScratchSize) + { + size_t read_len = len % kAesCtrScratchSize; + size_t read_pos = ((len / kAesCtrScratchSize) * kAesCtrScratchSize); + mFile.read(mScratch.getBytes() + mBlockOffset, read_len); + crypto::aes::AesCtr(mScratch.getBytes(), kAesCtrScratchAllocSize, mKey.key, mCurrentCtr.iv, mScratch.getBytes()); + memcpy(out + read_pos, mScratch.getBytes() + mBlockOffset, read_len); + } +} + +void AesCtrWrappedIFile::read(byte_t* out, size_t offset, size_t len) +{ + seek(offset); + read(out, len); +} + +void AesCtrWrappedIFile::write(const byte_t* out, size_t len) +{ + for (size_t i = 0; i < (len / kAesCtrScratchSize); i++) + { + memcpy(mScratch.getBytes() + mBlockOffset, out + (i * kAesCtrScratchSize), kAesCtrScratchSize); + crypto::aes::AesCtr(mScratch.getBytes(), kAesCtrScratchAllocSize, mKey.key, mCurrentCtr.iv, mScratch.getBytes()); + mFile.write(mScratch.getBytes() + mBlockOffset, kAesCtrScratchSize); + } + + if (len % kAesCtrScratchSize) + { + size_t write_len = len % kAesCtrScratchSize; + size_t write_pos = ((len / kAesCtrScratchSize) * kAesCtrScratchSize); + memcpy(mScratch.getBytes() + mBlockOffset, out + write_pos, write_len); + crypto::aes::AesCtr(mScratch.getBytes(), kAesCtrScratchAllocSize, mKey.key, mCurrentCtr.iv, mScratch.getBytes()); + mFile.write(mScratch.getBytes() + mBlockOffset, write_len); + } +} + +void AesCtrWrappedIFile::write(const byte_t* out, size_t offset, size_t len) +{ + seek(offset); + write(out, len); +} \ No newline at end of file diff --git a/programs/nstool/source/AesCtrWrappedIFile.h b/programs/nstool/source/AesCtrWrappedIFile.h new file mode 100644 index 0000000..5f88844 --- /dev/null +++ b/programs/nstool/source/AesCtrWrappedIFile.h @@ -0,0 +1,27 @@ +#include +#include +#include + +class AesCtrWrappedIFile : public fnd::IFile +{ +public: + AesCtrWrappedIFile(fnd::IFile& file, const crypto::aes::sAes128Key& key, const crypto::aes::sAesIvCtr& ctr); + + size_t size(); + void seek(size_t offset); + void read(byte_t* out, size_t len); + void read(byte_t* out, size_t offset, size_t len); + void write(const byte_t* out, size_t len); + void write(const byte_t* out, size_t offset, size_t len); +private: + const std::string kModuleName = "AesCtrWrappedIFile"; + static const size_t kAesCtrScratchSize = 0x1000000; + static const size_t kAesCtrScratchAllocSize = kAesCtrScratchSize + crypto::aes::kAesBlockSize; + + fnd::IFile& mFile; + crypto::aes::sAes128Key mKey; + crypto::aes::sAesIvCtr mBaseCtr, mCurrentCtr; + size_t mBlockOffset; + + fnd::MemoryBlob mScratch; +}; \ No newline at end of file diff --git a/programs/nstool/source/NcaPartitionProcess.cpp b/programs/nstool/source/NcaPartitionProcess.cpp new file mode 100644 index 0000000..e69de29 diff --git a/programs/nstool/source/NcaPartitionProcess.h b/programs/nstool/source/NcaPartitionProcess.h new file mode 100644 index 0000000..7424e34 --- /dev/null +++ b/programs/nstool/source/NcaPartitionProcess.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include +#include +#include + +#include "nstool.h" + +class NcaPartitionProcess +{ +public: + NcaPartitionProcess(); +private: + const std::string kModuleName = "NcaPartitionProcess"; + +}; \ No newline at end of file diff --git a/programs/nstool/source/NcaProcess.cpp b/programs/nstool/source/NcaProcess.cpp new file mode 100644 index 0000000..a954617 --- /dev/null +++ b/programs/nstool/source/NcaProcess.cpp @@ -0,0 +1,476 @@ +#include +#include +#include +#include "NcaProcess.h" +#include "PfsProcess.h" +#include "RomfsProcess.h" +#include "OffsetAdjustedIFile.h" +#include "AesCtrWrappedIFile.h" + +std::string kFormatVersionStr[] +{ + "NCA2", + "NCA3" +}; + +std::string kDistributionTypeStr[] +{ + "Download", + "Game Card" +}; + +std::string kContentTypeStr[] +{ + "Program", + "Meta", + "Control", + "Manual", + "Data", + "PublicData" +}; + +std::string kEncryptionTypeStr[] +{ + "Auto", + "None", + "AesXts", + "AesCtr", + "AesCtrEx" +}; + +std::string kHashTypeStr[] +{ + "Auto", + "None", + "HierarchicalSha256", + "HierarchicalIntegrity" +}; + +std::string kFormatTypeStr[] +{ + "RomFs", + "PartitionFs" +}; + +std::string kKaekIndexStr[] +{ + "Application", + "Ocean", + "System" +}; + +void NcaProcess::displayHeader() +{ + crypto::aes::sAes128Key zero_key; + memset(zero_key.key, 0, sizeof(zero_key)); + + printf("[NCA Header]\n"); + printf(" Format Type: %s\n", kFormatVersionStr[mHdr.getFormatVersion()].c_str()); + printf(" Dist. Type: %s\n", kDistributionTypeStr[mHdr.getDistributionType()].c_str()); + printf(" Content Type: %s\n", kContentTypeStr[mHdr.getContentType()].c_str()); + printf(" Key Generation: %d\n", mHdr.getKeyGeneration()); + printf(" Kaek Index: %s (%d)\n", kKaekIndexStr[mHdr.getKaekIndex()].c_str(), mHdr.getKaekIndex()); + printf(" Size: 0x%" PRIx64 "\n", mHdr.getContentSize()); + printf(" ProgID: 0x%016" PRIx64 "\n", mHdr.getProgramId()); + printf(" Content Index: %" PRIu32 "\n", mHdr.getContentIndex()); + uint32_t ver = mHdr.getSdkAddonVersion(); + printf(" SdkAddon Ver.: v%d.%d.%d (v%" PRIu32 ")\n", (ver>>24 & 0xff),(ver>>16 & 0xff),(ver>>8 & 0xff), ver); + printf(" RightsId: "); + fnd::SimpleTextOutput::hexDump(mHdr.getRightsId(), nx::nca::kRightsIdLen); + printf(" Key Area Keys: (Encrypted)\n"); + for (size_t i = 0; i < mHdr.getEncAesKeys().getSize(); i++) + { + if (mHdr.getEncAesKeys()[i] != zero_key) + { + printf(" %2lu: ", i); + fnd::SimpleTextOutput::hexDump(mHdr.getEncAesKeys()[i].key, crypto::aes::kAes128KeySize); + } + } + + /* + if (mBodyKeyList.getSize() > 0) + { + printf(" Key Area Keys:\n"); + for (size_t i = 0; i < mBodyKeyList.getSize(); i++) + { + printf(" %2lu: ", i); + fnd::SimpleTextOutput::hexDump(mBodyKeyList[i].key, crypto::aes::kAes128KeySize); + } + } + */ + + printf(" Partitions:\n"); + for (size_t i = 0; i < mHdr.getPartitions().getSize(); i++) + { + const nx::NcaHeader::sPartition& partition = mHdr.getPartitions()[i]; + nx::sNcaFsHeader& fs_header = mHdrBlock.fs_header[partition.index]; + + printf(" %lu:\n", i); + printf(" Index: %d\n", partition.index); + printf(" Offset: 0x%" PRIx64 "\n", partition.offset); + printf(" Size: 0x%" PRIx64 "\n", partition.size); + + + crypto::sha::sSha256Hash ncaFsHeaderHash; + crypto::sha::Sha256((byte_t*)&fs_header, sizeof(nx::sNcaFsHeader), ncaFsHeaderHash.bytes); + if (partition.hash.compare(ncaFsHeaderHash) == false) + { + throw fnd::Exception(kModuleName, "NcaFsHeader has bad sha256 hash"); + } + + //fnd::SimpleTextOutput::hxdStyleDump((byte_t*)&fs_header, sizeof(nx::sNcaFsHeader)); + + + printf(" FsHeader:\n"); + printf(" Version: 0x%d\n", fs_header.version.get()); + printf(" Format Type: %s\n", kFormatTypeStr[fs_header.format_type].c_str()); + printf(" Hash Type: %s\n", kHashTypeStr[fs_header.hash_type].c_str()); + printf(" Enc. Type: %s\n", kEncryptionTypeStr[fs_header.encryption_type].c_str()); + if (fs_header.encryption_type == nx::nca::CRYPT_AESCTR) + { + printf(" CTR: "); + crypto::aes::sAesIvCtr ctr; + nx::NcaUtils::getNcaPartitionAesCtr(&fs_header, ctr.iv); + crypto::aes::AesIncrementCounter(ctr.iv, partition.offset>>4, ctr.iv); + fnd::SimpleTextOutput::hexDump(ctr.iv, sizeof(crypto::aes::sAesIvCtr)); + } + if (fs_header.hash_type == nx::nca::HASH_HIERARCHICAL_INTERGRITY) + { + printf(" HierarchicalIntegrity Header:\n"); + printf(" Id: 0x%x\n", fs_header.ivfc_header.id.get()); + printf(" MasterHashSize: 0x%x\n", fs_header.ivfc_header.master_hash_size.get()); + printf(" LevelNum: %d\n", fs_header.ivfc_header.level_num.get()); + for (size_t i = 0; i < fs_header.ivfc_header.level_num.get(); i++) + { + printf(" Level %d:\n", i); + printf(" LogicalOffset: 0x%" PRIx64 "\n", fs_header.ivfc_header.level_header[i].logical_offset.get()); + printf(" HashDataSize: 0x%" PRIx64 "\n", fs_header.ivfc_header.level_header[i].hash_data_size.get()); + printf(" BlockSize: 0x%" PRIx32 "\n", fs_header.ivfc_header.level_header[i].block_size.get()); + } + printf(" Master Hash: "); + fnd::SimpleTextOutput::hexDump(fs_header.ivfc_header.master_hash.bytes, 0x20); + + + } + else if (fs_header.hash_type == nx::nca::HASH_HIERARCHICAL_SHA256) + { + nx::sHierarchicalSha256Header& hash_hdr = fs_header.hierarchicalsha256_header; + printf(" HierarchicalSha256 Header:\n"); + printf(" Master Hash: "); + fnd::SimpleTextOutput::hexDump(hash_hdr.master_hash.bytes, 0x20); + printf(" HashBlockSize: 0x%x\n", hash_hdr.hash_block_size.get()); + printf(" HashLevelNum: 0x%x\n", hash_hdr.hash_level_num.get()); + printf(" HashDataOffset: 0x%" PRIx64 "\n", hash_hdr.hash_data.offset.get()); + printf(" HashDataSize: 0x%" PRIx64 "\n", hash_hdr.hash_data.size.get()); + printf(" HashTargetOffset: 0x%" PRIx64 "\n", hash_hdr.hash_target.offset.get()); + printf(" HashTargetSize: 0x%" PRIx64 "\n", hash_hdr.hash_target.size.get()); + + } + else + { + printf(" Hash Superblock:\n"); + fnd::SimpleTextOutput::hxdStyleDump(fs_header.hash_superblock, nx::nca::kFsHeaderHashSuperblockLen); + } + } +} + +void NcaProcess::generateNcaBodyEncryptionKeys() +{ + // create zeros key + crypto::aes::sAes128Key zero_aesctr_key; + memset(zero_aesctr_key.key, 0, sizeof(zero_aesctr_key)); + crypto::aes::sAesXts128Key zero_aesxts_key; + memset(zero_aesxts_key.key, 0, sizeof(zero_aesxts_key)); + + // get key data from header + byte_t masterkey_rev = nx::NcaUtils::getMasterKeyRevisionFromKeyGeneration(mHdr.getKeyGeneration()); + byte_t keak_index = mHdr.getKaekIndex(); + + // set flag to indicate that the keys are not available + mBodyKeys.aes_ctr.isSet = false; + mBodyKeys.aes_xts.isSet = false; + + // if this has a rights id, the key needs to be sourced from a ticket + if (mHdr.hasRightsId() == true) + { + // if the titlekey_kek is available + if (mKeyset->ticket.titlekey_kek[masterkey_rev] != zero_aesctr_key) + { + // the title key is provided (sourced from ticket) + if (mKeyset->nca.manual_title_key_aesctr != zero_aesctr_key) + { + nx::AesKeygen::generateKey(mBodyKeys.aes_ctr.var.key, mKeyset->nca.manual_title_key_aesctr.key, mKeyset->ticket.titlekey_kek[masterkey_rev].key); + mBodyKeys.aes_ctr.isSet = true; + } + if (mKeyset->nca.manual_title_key_aesxts != zero_aesxts_key) + { + nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[0], mKeyset->nca.manual_title_key_aesxts.key[0], mKeyset->ticket.titlekey_kek[masterkey_rev].key); + nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[1], mKeyset->nca.manual_title_key_aesxts.key[1], mKeyset->ticket.titlekey_kek[masterkey_rev].key); + mBodyKeys.aes_xts.isSet = true; + } + } + } + // otherwise decrypt key area + else + { + // if the titlekey_kek is available + if (mKeyset->nca.key_area_key[keak_index][masterkey_rev] != zero_aesctr_key) + { + nx::AesKeygen::generateKey(mBodyKeys.aes_ctr.var.key, mHdr.getEncAesKeys()[nx::nca::KEY_AESCTR].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key); + mBodyKeys.aes_ctr.isSet = true; + + nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[0], mHdr.getEncAesKeys()[nx::nca::KEY_AESXTS_0].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key); + nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[1], mHdr.getEncAesKeys()[nx::nca::KEY_AESXTS_1].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key); + mBodyKeys.aes_xts.isSet = true; + } + } + + // if the keys weren't generated, check if the keys were supplied by the user + if (mBodyKeys.aes_ctr.isSet == false && mKeyset->nca.manual_body_key_aesctr != zero_aesctr_key) + { + mBodyKeys.aes_ctr = mKeyset->nca.manual_body_key_aesctr; + } + if (mBodyKeys.aes_xts.isSet == false && mKeyset->nca.manual_body_key_aesxts != zero_aesxts_key) + { + mBodyKeys.aes_xts = mKeyset->nca.manual_body_key_aesxts; + } +} + +void NcaProcess::processPartitions() +{ + for (size_t i = 0; i < mHdr.getPartitions().getSize(); i++) + { + const nx::NcaHeader::sPartition& partition = mHdr.getPartitions()[i]; + nx::sNcaFsHeader& fs_header = mHdrBlock.fs_header[partition.index]; + + crypto::aes::sAesIvCtr ctr; + nx::NcaUtils::getNcaPartitionAesCtr(&fs_header, ctr.iv); + + // create reader + fnd::IFile* partitionReader = nullptr; + + AesCtrWrappedIFile aesCtrFile = AesCtrWrappedIFile(*mReader, mBodyKeys.aes_ctr.var, ctr); + switch(fs_header.encryption_type) + { + case (nx::nca::CRYPT_AESXTS): + case (nx::nca::CRYPT_AESCTREX): + partitionReader = nullptr; + break; + case (nx::nca::CRYPT_AESCTR): + partitionReader = mBodyKeys.aes_ctr.isSet? &aesCtrFile : nullptr; + break; + case (nx::nca::CRYPT_NONE): + partitionReader = mReader; + break; + } + + // if the reader is null, skip + if (partitionReader == nullptr) + { + printf("[WARNING] NCA Partition %d not readable\n", partition.index); + continue; + } + + size_t data_offset = 0; + switch (fs_header.hash_type) + { + case (nx::nca::HASH_HIERARCHICAL_SHA256): + data_offset = fs_header.hierarchicalsha256_header.hash_target.offset.get(); + break; + case (nx::nca::HASH_HIERARCHICAL_INTERGRITY): + data_offset = fs_header.ivfc_header.level_header[5].logical_offset.get(); + break; + case (nx::nca::HASH_NONE): + data_offset = 0; + break; + default: + throw fnd::Exception(kModuleName, "Unknown hash type"); + } + + if (fs_header.format_type == nx::nca::FORMAT_PFS0) + { + PfsProcess pfs; + pfs.setInputFile(*partitionReader); + pfs.setInputFileOffset(partition.offset + data_offset); + pfs.setCliOutputMode(mCliOutputType); + pfs.setListFs(mListFs); + if (mPartitionPath[partition.index].doExtract) + pfs.setExtractPath(mPartitionPath[partition.index].path); + pfs.process(); + } + else if (fs_header.format_type == nx::nca::FORMAT_ROMFS) + { + RomfsProcess romfs; + romfs.setInputFile(*partitionReader); + romfs.setInputFileOffset(partition.offset + data_offset); + romfs.setCliOutputMode(mCliOutputType); + romfs.setListFs(mListFs); + if (mPartitionPath[partition.index].doExtract) + romfs.setExtractPath(mPartitionPath[partition.index].path); + romfs.process(); + } + else + { + throw fnd::Exception(kModuleName, "Unknown format type"); + } + } +} + +NcaProcess::NcaProcess() : + mReader(nullptr), + mOffset(0), + mKeyset(nullptr), + mCliOutputType(OUTPUT_NORMAL), + mVerify(false), + mListFs(false) +{ + mPartitionPath[0].doExtract = false; + mPartitionPath[1].doExtract = false; + mPartitionPath[2].doExtract = false; + mPartitionPath[3].doExtract = false; +} + +NcaProcess::~NcaProcess() +{ + +} + +void NcaProcess::process() +{ + fnd::MemoryBlob scratch; + + if (mReader == nullptr) + { + throw fnd::Exception(kModuleName, "No file reader set."); + } + + // read header block + mReader->read((byte_t*)&mHdrBlock, mOffset, sizeof(nx::sNcaHeaderBlock)); + + // decrypt header block + nx::NcaUtils::decryptNcaHeader((byte_t*)&mHdrBlock, (byte_t*)&mHdrBlock, mKeyset->nca.header_key); + + // generate header hash + crypto::sha::Sha256((byte_t*)&mHdrBlock.header, sizeof(nx::sNcaHeader), mHdrHash.bytes); + + // validate signature[0] + if (mVerify) + { + if (crypto::rsa::pss::rsaVerify(mKeyset->nca.header_sign_key, crypto::sha::HASH_SHA256, mHdrHash.bytes, mHdrBlock.signature_main) != 0) + { + // this is minimal even though it's a warning because it's a validation method + if (mCliOutputType >= OUTPUT_MINIMAL) + printf("[WARNING] NCA Header Main Signature: FAIL \n"); + } + } + + // proccess main header + mHdr.importBinary((byte_t*)&mHdrBlock.header, sizeof(nx::sNcaHeader)); + + // validate fs headers + if (mVerify) + { + crypto::sha::sSha256Hash calc_hash; + + for (size_t i = 0; i < mHdr.getPartitions().getSize(); i++) + { + const nx::NcaHeader::sPartition& partition = mHdr.getPartitions()[i]; + + crypto::sha::Sha256((const byte_t*)&mHdrBlock.fs_header[partition.index], sizeof(nx::sNcaFsHeader), calc_hash.bytes); + + if (calc_hash.compare(partition.hash) == false) + { + // this is minimal even though it's a warning because it's a validation method + if (mCliOutputType >= OUTPUT_MINIMAL) + printf("[WARNING] NCA FsHeader[%d] Hash: FAIL \n", partition.index); + } + } + } + + // determine keys + + + /* + NCA is a file container + A hashed and signed file container + + To verify a NCA: (R=regular step) + 1 - decrypt header (R) + 2 - verify signature[0] + 3 - validate hashes of fs_headers + 4 - determine how to read/decrypt the partitions (R) + 5 - validate the partitions depending on their hash method + 6 - if this NCA is a Program or Patch, open main.npdm from partition0 + 7 - validate ACID + 8 - use public key in ACID to verify NCA signature[1] + + Things to consider + * because of the manditory steps between verifcation steps + the NCA should be ready to be pulled to pieces before any printing is done + so the verification text can be presented without interuption + + */ + + + // decrypt key area + generateNcaBodyEncryptionKeys(); + + if (mCliOutputType >= OUTPUT_NORMAL) + displayHeader(); + + processPartitions(); +} + +void NcaProcess::setInputFile(fnd::IFile* reader) +{ + mReader = reader; +} + +void NcaProcess::setInputFileOffset(size_t offset) +{ + mOffset = offset; +} + +void NcaProcess::setKeyset(const sKeyset* keyset) +{ + mKeyset = keyset; +} + +void NcaProcess::setCliOutputMode(CliOutputType type) +{ + mCliOutputType = type; +} + +void NcaProcess::setVerifyMode(bool verify) +{ + mVerify = verify; +} + +void NcaProcess::setPartition0ExtractPath(const std::string& path) +{ + mPartitionPath[0].path = path; + mPartitionPath[0].doExtract = true; +} + +void NcaProcess::setPartition1ExtractPath(const std::string& path) +{ + mPartitionPath[1].path = path; + mPartitionPath[1].doExtract = true; +} + +void NcaProcess::setPartition2ExtractPath(const std::string& path) +{ + mPartitionPath[2].path = path; + mPartitionPath[2].doExtract = true; +} + +void NcaProcess::setPartition3ExtractPath(const std::string& path) +{ + mPartitionPath[3].path = path; + mPartitionPath[3].doExtract = true; +} + +void NcaProcess::setListFs(bool list_fs) +{ + mListFs = list_fs; +} diff --git a/programs/nstool/source/NcaProcess.h b/programs/nstool/source/NcaProcess.h index 610ad46..0d9890b 100644 --- a/programs/nstool/source/NcaProcess.h +++ b/programs/nstool/source/NcaProcess.h @@ -2,7 +2,6 @@ #include #include #include -#include #include #include "nstool.h" @@ -16,18 +15,54 @@ public: void process(); // generic - void setInputFile(fnd::IFile& reader); + void setInputFile(fnd::IFile* reader); void setInputFileOffset(size_t offset); void setKeyset(const sKeyset* keyset); void setCliOutputMode(CliOutputType type); void setVerifyMode(bool verify); // nca specfic + void setPartition0ExtractPath(const std::string& path); + void setPartition1ExtractPath(const std::string& path); + void setPartition2ExtractPath(const std::string& path); + void setPartition3ExtractPath(const std::string& path); + void setListFs(bool list_fs); private: const std::string kModuleName = "NcaProcess"; - byte_t mRawHeader[nx::nca::kHeaderSize]; - std::string mPath; + // user options + fnd::IFile* mReader; + size_t mOffset; const sKeyset* mKeyset; + CliOutputType mCliOutputType; + bool mVerify; + + struct sExtract + { + std::string path; + bool doExtract; + } mPartitionPath[nx::nca::kPartitionNum]; + + bool mListFs; + + // data + nx::sNcaHeaderBlock mHdrBlock; + crypto::sha::sSha256Hash mHdrHash; + nx::NcaHeader mHdr; + + // crypto + struct sKeys + { + sOptional aes_ctr; + sOptional aes_xts; + } mBodyKeys; + + + + void displayHeader(); + + void generateNcaBodyEncryptionKeys(); + + void processPartitions(); }; \ No newline at end of file diff --git a/programs/nstool/source/OffsetAdjustedIFile.cpp b/programs/nstool/source/OffsetAdjustedIFile.cpp new file mode 100644 index 0000000..fdf7cdb --- /dev/null +++ b/programs/nstool/source/OffsetAdjustedIFile.cpp @@ -0,0 +1,45 @@ +#include "OffsetAdjustedIFile.h" + +OffsetAdjustedIFile::OffsetAdjustedIFile(fnd::IFile& file, size_t offset, size_t size) : + mFile(file), + mBaseOffset(offset), + mCurrentOffset(0), + mSize(size) +{ + +} + +size_t OffsetAdjustedIFile::size() +{ + return mSize; +} + +void OffsetAdjustedIFile::seek(size_t offset) +{ + mCurrentOffset = offset; + mFile.seek(offset + mBaseOffset); +} + +void OffsetAdjustedIFile::read(byte_t* out, size_t len) +{ + seek(mCurrentOffset); + mFile.read(out, len); +} + +void OffsetAdjustedIFile::read(byte_t* out, size_t offset, size_t len) +{ + seek(offset); + read(out, len); +} + +void OffsetAdjustedIFile::write(const byte_t* out, size_t len) +{ + seek(mCurrentOffset); + mFile.write(out, len); +} + +void OffsetAdjustedIFile::write(const byte_t* out, size_t offset, size_t len) +{ + seek(offset); + write(out, len); +} \ No newline at end of file diff --git a/programs/nstool/source/OffsetAdjustedIFile.h b/programs/nstool/source/OffsetAdjustedIFile.h new file mode 100644 index 0000000..18b3b9c --- /dev/null +++ b/programs/nstool/source/OffsetAdjustedIFile.h @@ -0,0 +1,18 @@ +#include + +class OffsetAdjustedIFile : public fnd::IFile +{ +public: + OffsetAdjustedIFile(fnd::IFile& file, size_t offset, size_t size); + + size_t size(); + void seek(size_t offset); + void read(byte_t* out, size_t len); + void read(byte_t* out, size_t offset, size_t len); + void write(const byte_t* out, size_t len); + void write(const byte_t* out, size_t offset, size_t len); +private: + fnd::IFile& mFile; + size_t mBaseOffset, mCurrentOffset; + size_t mSize; +}; \ No newline at end of file diff --git a/programs/nstool/source/PfsProcess.cpp b/programs/nstool/source/PfsProcess.cpp index 10656d7..8577e8f 100644 --- a/programs/nstool/source/PfsProcess.cpp +++ b/programs/nstool/source/PfsProcess.cpp @@ -73,9 +73,13 @@ void PfsProcess::extractFs() fnd::SimpleFile outFile; const fnd::List& file = mPfs.getFileList(); + std::string file_path; for (size_t i = 0; i < file.getSize(); i++) { - outFile.open(mExtractPath + kPathSeparator + file[i].name, outFile.Create); + file_path.clear(); + fnd::io::appendToPath(file_path, mExtractPath); + fnd::io::appendToPath(file_path, file[i].name); + outFile.open(file_path, outFile.Create); mReader->seek(mOffset + file[i].offset); for (size_t j = 0; j < (file[i].size / kFileExportBlockSize); j++) { diff --git a/programs/nstool/source/UserSettings.cpp b/programs/nstool/source/UserSettings.cpp index d95993d..d50db04 100644 --- a/programs/nstool/source/UserSettings.cpp +++ b/programs/nstool/source/UserSettings.cpp @@ -52,7 +52,6 @@ void UserSettings::showHelp() printf(" nstool [--listfs] [--fsdir ] \n"); printf(" --listfs Print file system\n"); printf(" --fsdir Extract file system to directory\n"); - /* printf("\n NCA (Nintendo Content Archive)\n"); printf(" nstool [--listfs] [--bodykey --titlekey ] [--part0 ...] <.nca file>\n"); printf(" --listfs Print file system in embedded partitions\n"); @@ -62,7 +61,6 @@ void UserSettings::showHelp() printf(" --part1 Extract \"partition 1\" to directory \n"); printf(" --part2 Extract \"partition 2\" to directory \n"); printf(" --part3 Extract \"partition 3\" to directory \n"); - */ } const std::string UserSettings::getInputPath() const @@ -115,6 +113,26 @@ const sOptional& UserSettings::getFsPath() const return mFsPath; } +const sOptional& UserSettings::getPart0Path() const +{ + return mPart0Path; +} + +const sOptional& UserSettings::getPart1Path() const +{ + return mPart1Path; +} + +const sOptional& UserSettings::getPart2Path() const +{ + return mPart2Path; +} + +const sOptional& UserSettings::getPart3Path() const +{ + return mPart3Path; +} + void UserSettings::populateCmdArgs(int argc, char** argv, sCmdArgs& cmd_args) { @@ -226,10 +244,28 @@ void UserSettings::populateCmdArgs(int argc, char** argv, sCmdArgs& cmd_args) cmd_args.nca_bodykey = args[i+1]; } - else if (args[i] == "-o") + else if (args[i] == "--part0") { if (!hasParamter) throw fnd::Exception(kModuleName, args[i] + " requries a parameter."); - cmd_args.output_path = args[i+1]; + cmd_args.part0_path = args[i+1]; + } + + else if (args[i] == "--part1") + { + if (!hasParamter) throw fnd::Exception(kModuleName, args[i] + " requries a parameter."); + cmd_args.part1_path = args[i+1]; + } + + else if (args[i] == "--part2") + { + if (!hasParamter) throw fnd::Exception(kModuleName, args[i] + " requries a parameter."); + cmd_args.part2_path = args[i+1]; + } + + else if (args[i] == "--part3") + { + if (!hasParamter) throw fnd::Exception(kModuleName, args[i] + " requries a parameter."); + cmd_args.part3_path = args[i+1]; } else @@ -393,7 +429,14 @@ void UserSettings::populateKeyset(sCmdArgs& args) if (args.nca_titlekey.isSet) { - decodeHexStringToBytes("--titlekey", args.nca_titlekey.var, mKeyset.nca.manual_title_key.key, sizeof(crypto::aes::sAes128Key)); + if (args.nca_bodykey.var.length() == (sizeof(crypto::aes::sAes128Key)*2)) + { + decodeHexStringToBytes("--titlekey", args.nca_titlekey.var, mKeyset.nca.manual_title_key_aesctr.key, sizeof(crypto::aes::sAes128Key)); + } + else + { + decodeHexStringToBytes("--titlekey", args.nca_titlekey.var, mKeyset.nca.manual_title_key_aesxts.key[0], sizeof(crypto::aes::sAesXts128Key)); + } } #undef _SAVE_KEYDATA @@ -475,6 +518,10 @@ void UserSettings::populateUserSettings(sCmdArgs& args) mNormalPath = args.normal_path; mSecurePath = args.secure_path; mFsPath = args.fs_path; + mPart0Path = args.part0_path; + mPart1Path = args.part1_path; + mPart2Path = args.part2_path; + mPart3Path = args.part3_path; // determine output path if (args.verbose_output.isSet) diff --git a/programs/nstool/source/UserSettings.h b/programs/nstool/source/UserSettings.h index fbd1afe..030d6d5 100644 --- a/programs/nstool/source/UserSettings.h +++ b/programs/nstool/source/UserSettings.h @@ -26,6 +26,10 @@ public: const sOptional& getNormalPath() const; const sOptional& getSecurePath() const; const sOptional& getFsPath() const; + const sOptional& getPart0Path() const; + const sOptional& getPart1Path() const; + const sOptional& getPart2Path() const; + const sOptional& getPart3Path() const; private: const std::string kModuleName = "UserSettings"; @@ -33,7 +37,6 @@ private: struct sCmdArgs { sOptional input_path; - sOptional output_path; sOptional devkit_keys; sOptional keyset_path; sOptional file_type; @@ -47,11 +50,14 @@ private: sOptional fs_path; sOptional nca_titlekey; sOptional nca_bodykey; + sOptional part0_path; + sOptional part1_path; + sOptional part2_path; + sOptional part3_path; void clear() { input_path.isSet = false; - output_path.isSet = false; devkit_keys.isSet = false; keyset_path.isSet = false; file_type.isSet = false; @@ -65,6 +71,10 @@ private: fs_path.isSet = false; nca_titlekey.isSet = false; nca_bodykey.isSet = false; + part0_path.isSet = false; + part1_path.isSet = false; + part2_path.isSet = false; + part3_path.isSet = false; } }; @@ -80,6 +90,11 @@ private: sOptional mSecurePath; sOptional mFsPath; + sOptional mPart0Path; + sOptional mPart1Path; + sOptional mPart2Path; + sOptional mPart3Path; + void populateCmdArgs(int argc, char** argv, sCmdArgs& cmd_args); void populateKeyset(sCmdArgs& args); void populateUserSettings(sCmdArgs& args); diff --git a/programs/nstool/source/main.cpp b/programs/nstool/source/main.cpp index ca22050..8296f9a 100644 --- a/programs/nstool/source/main.cpp +++ b/programs/nstool/source/main.cpp @@ -4,7 +4,7 @@ #include "XciProcess.h" #include "PfsProcess.h" #include "RomfsProcess.h" -//#include "NcaProcess.h" +#include "NcaProcess.h" #include "NpdmProcess.h" @@ -54,7 +54,6 @@ int main(int argc, char** argv) } else if (user_set.getFileType() == FILE_ROMFS) { - RomfsProcess romfs; romfs.setInputFile(inputFile); @@ -67,20 +66,28 @@ int main(int argc, char** argv) romfs.setListFs(user_set.isListFs()); romfs.process(); - } else if (user_set.getFileType() == FILE_NCA) { - /* NcaProcess nca; - nca.setNcaPath(user_set.getInputPath()); - nca.setKeyset(user_set.getKeyset()); + nca.setInputFile(&inputFile); + nca.setKeyset(&user_set.getKeyset()); nca.setCliOutputMode(user_set.getCliOutputType()); nca.setVerifyMode(user_set.isVerifyFile()); + + if (user_set.getPart0Path().isSet) + nca.setPartition0ExtractPath(user_set.getPart0Path().var); + if (user_set.getPart1Path().isSet) + nca.setPartition1ExtractPath(user_set.getPart1Path().var); + if (user_set.getPart2Path().isSet) + nca.setPartition2ExtractPath(user_set.getPart2Path().var); + if (user_set.getPart3Path().isSet) + nca.setPartition3ExtractPath(user_set.getPart3Path().var); + nca.setListFs(user_set.isListFs()); + nca.process(); - */ } else if (user_set.getFileType() == FILE_NPDM) { diff --git a/programs/nstool/source/nstool.h b/programs/nstool/source/nstool.h index a411172..049cec9 100644 --- a/programs/nstool/source/nstool.h +++ b/programs/nstool/source/nstool.h @@ -9,11 +9,6 @@ static const size_t kMasterKeyNum = 0x20; static const size_t kNcaKeakNum = nx::nca::kKeyAreaEncryptionKeyNum; -#ifdef _WIN32 -const std::string kPathSeparator = "\\"; -#else -const std::string kPathSeparator = "/"; -#endif enum FileType { @@ -63,7 +58,8 @@ struct sKeyset crypto::aes::sAesXts128Key header_key; crypto::aes::sAes128Key key_area_key[kNcaKeakNum][kMasterKeyNum]; - crypto::aes::sAes128Key manual_title_key; + crypto::aes::sAes128Key manual_title_key_aesctr; + crypto::aes::sAesXts128Key manual_title_key_aesxts; crypto::aes::sAes128Key manual_body_key_aesctr; crypto::aes::sAesXts128Key manual_body_key_aesxts; } nca;