diff --git a/NXTools.sln b/NXTools.sln index 888f501..fdb13e0 100644 --- a/NXTools.sln +++ b/NXTools.sln @@ -13,6 +13,21 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ncatool", "programs\ncatool EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{170B4A09-1B67-4A62-93AB-116EBCFF4A8C}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "npdmtool", "programs\npdmtool\npdmtool.vcxproj", "{550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Programs", "Programs", "{E0863FCC-8E72-490D-BE1B-458F12CA8298}" + ProjectSection(SolutionItems) = preProject + programs\makefile = programs\makefile + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8F6C846D-35E2-47FD-AF42-7A3FD036346E}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + makefile = makefile + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "pfstool", "programs\pfstool\pfstool.vcxproj", "{BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -53,6 +68,22 @@ Global {7DA88C6F-4470-495D-995A-4F633F3370C1}.Release|x64.Build.0 = Release|x64 {7DA88C6F-4470-495D-995A-4F633F3370C1}.Release|x86.ActiveCfg = Release|Win32 {7DA88C6F-4470-495D-995A-4F633F3370C1}.Release|x86.Build.0 = Release|Win32 + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}.Debug|x64.ActiveCfg = Debug|x64 + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}.Debug|x64.Build.0 = Debug|x64 + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}.Debug|x86.ActiveCfg = Debug|Win32 + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}.Debug|x86.Build.0 = Debug|Win32 + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}.Release|x64.ActiveCfg = Release|x64 + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}.Release|x64.Build.0 = Release|x64 + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}.Release|x86.ActiveCfg = Release|Win32 + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C}.Release|x86.Build.0 = Release|Win32 + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}.Debug|x64.ActiveCfg = Debug|x64 + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}.Debug|x64.Build.0 = Debug|x64 + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}.Debug|x86.ActiveCfg = Debug|Win32 + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}.Debug|x86.Build.0 = Debug|Win32 + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}.Release|x64.ActiveCfg = Release|x64 + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}.Release|x64.Build.0 = Release|x64 + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}.Release|x86.ActiveCfg = Release|Win32 + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -61,5 +92,8 @@ Global {4D27EDB9-5110-44FE-8CE2-D46C5AD3C55B} = {170B4A09-1B67-4A62-93AB-116EBCFF4A8C} {6ADBB60D-DBA0-411D-BD2D-A355EF8E0FE1} = {170B4A09-1B67-4A62-93AB-116EBCFF4A8C} {91BA9E79-8242-4F7D-B997-0DFEC95EA22B} = {170B4A09-1B67-4A62-93AB-116EBCFF4A8C} + {7DA88C6F-4470-495D-995A-4F633F3370C1} = {E0863FCC-8E72-490D-BE1B-458F12CA8298} + {550C6AC3-EBE0-46CA-AE6C-EEEB59DDF35C} = {E0863FCC-8E72-490D-BE1B-458F12CA8298} + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB} = {E0863FCC-8E72-490D-BE1B-458F12CA8298} EndGlobalSection EndGlobal diff --git a/lib/fnd/List.h b/lib/fnd/List.h index b71de0d..b4dd398 100644 --- a/lib/fnd/List.h +++ b/lib/fnd/List.h @@ -73,20 +73,45 @@ namespace fnd // functions void addElement(const T& element) { mElements.push_back(element); } - size_t getIndexOf(const T& element) const + size_t getIndexOf(const T& key) const { for (size_t i = 0; i < getSize(); i++) { - if (getElement(i) == element) return i; + if (getElement(i) == key) return i; } throw Exception("LIST", "Element does not exist"); } - bool hasElement(const T& element) const + bool hasElement(const T& key) const { try { - getIndexOf(element); + getIndexOf(key); + } catch (const Exception&) + { + return false; + } + + return true; + } + + // special + template + size_t getIndexOf(const X& key) const + { + for (size_t i = 0; i < getSize(); i++) + { + if (getElement(i) == key) return i; + } + + throw Exception("LIST", "Element does not exist"); + } + template + bool hasElement(const X& key) const + { + try + { + getIndexOf(key); } catch (const Exception&) { return false; diff --git a/lib/nx/AcidBinary.cpp b/lib/nx/AcidBinary.cpp index f7f3432..3224405 100644 --- a/lib/nx/AcidBinary.cpp +++ b/lib/nx/AcidBinary.cpp @@ -23,12 +23,12 @@ void nx::AcidBinary::clear() mEmbeddedPublicKey = crypto::rsa::sRsa2048Key(); } -const crypto::rsa::sRsa2048Key & nx::AcidBinary::getPublicKey() const +const crypto::rsa::sRsa2048Key & nx::AcidBinary::getNcaHeader2RsaKey() const { return mEmbeddedPublicKey; } -void nx::AcidBinary::setPublicKey(const crypto::rsa::sRsa2048Key & key) +void nx::AcidBinary::setNcaHeader2RsaKey(const crypto::rsa::sRsa2048Key & key) { mEmbeddedPublicKey = key; } diff --git a/lib/nx/AcidBinary.h b/lib/nx/AcidBinary.h index 5ee1c1f..795ee7c 100644 --- a/lib/nx/AcidBinary.h +++ b/lib/nx/AcidBinary.h @@ -32,8 +32,8 @@ namespace nx // variables virtual void clear(); - const crypto::rsa::sRsa2048Key& getPublicKey() const; - void setPublicKey(const crypto::rsa::sRsa2048Key& key); + const crypto::rsa::sRsa2048Key& getNcaHeader2RsaKey() const; + void setNcaHeader2RsaKey(const crypto::rsa::sRsa2048Key& key); private: const std::string kModuleName = "ACID_BINARY"; diff --git a/lib/nx/NXCrypto.h b/lib/nx/NXCrypto.h index ca2e6e8..4fdc9aa 100644 --- a/lib/nx/NXCrypto.h +++ b/lib/nx/NXCrypto.h @@ -46,6 +46,8 @@ namespace crypto TitleKeyGenarateKey }; + u8 titlekey_generate_key[0x20] = { 39, 111, 56, 188, 68, 106, 241, 86, 31, 44, 90, 111, 116, 32, 93, 197, 25, 181, 59, 188, 178, 159, 211, 175, 212, 178, 162, 4, 28, 152, 117, 126 }; + // aes128-cbc keys u8 xci_header_key[16] = { 0x01, 0xc5, 0x8f, 0xe7, 0x2d, 0x13, 0x5a, 0xb2, 0x9a, 0x3f, 0x69, 0x33, 0x95, 0x74, 0xb1 }; u8 eticket_common_key[16] = { 0x55, 0xA3, 0xF8, 0x72, 0xBD, 0xC8, 0x0C, 0x55, 0x5A, 0x65, 0x43, 0x81, 0x13, 0x9E, 0x15, 0x3B }; // lol this 3ds dev common key @@ -69,7 +71,7 @@ namespace crypto { 0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2 } }; // aeskey, related to m_KeyAreaEncryptionKeyList (first in list?) - u8 unk_aes_key[0x10] = { 0x3A, 0x7C, 0x3E, 0x38, 0x4A, 0x8F, 0x22, 0xFF, 0x4B, 0x21, 0x57, 0x19, 0xB7, 0x81, 0xAD, 0x0C }; + u8 key_area_encryption_key_0[0x10] = { 0x3A, 0x7C, 0x3E, 0x38, 0x4A, 0x8F, 0x22, 0xFF, 0x4B, 0x21, 0x57, 0x19, 0xB7, 0x81, 0xAD, 0x0C }; } } diff --git a/lib/nx/NcaHeader.cpp b/lib/nx/NcaHeader.cpp index 55dbd17..7f51aaa 100644 --- a/lib/nx/NcaHeader.cpp +++ b/lib/nx/NcaHeader.cpp @@ -10,10 +10,14 @@ void NcaHeader::exportBinary() sNcaHeader* hdr = (sNcaHeader*)mBinaryBlob.getBytes(); hdr->set_signature(kNcaSig.c_str()); - hdr->set_block_size(kDefaultBlockSize); + hdr->set_distribution_type(mDistributionType); + hdr->set_content_type(mContentType); + hdr->set_key_generation(mEncryptionType); + hdr->set_key_area_encryption_key_index(mKeyIndex); hdr->set_nca_size(mNcaSize); hdr->set_program_id(mProgramId); - hdr->set_unk0(mUnk0); + hdr->set_content_index(mContentIndex); + hdr->set_sdk_addon_version(mSdkAddonVersion); // TODO: properly reconstruct NCA layout? atm in hands of user @@ -24,13 +28,13 @@ void NcaHeader::exportBinary() hdr->section(section).set_start(sizeToBlockNum(mSections[i].offset)); hdr->section(section).set_end(sizeToBlockNum(mSections[i].offset) + sizeToBlockNum(mSections[i].size)); - hdr->section(section).set_key_type(mSections[i].key_type); + hdr->section(section).set_enabled(1); hdr->section_hash(section) = mSections[i].hash; } for (size_t i = 0; i < kAesKeyNum; i++) { - hdr->aes_key(i) = mAesKeys[i]; + hdr->enc_aes_key(i) = mEncAesKeys[i]; } } @@ -53,10 +57,14 @@ void NcaHeader::importBinary(const u8 * bytes, size_t len) throw fnd::Exception(kModuleName, "NCA header corrupt"); } - mBlockSize = hdr->block_size(); + mDistributionType = (DistributionType)hdr->distribution_type(); + mContentType = (ContentType)hdr->content_type(); + mEncryptionType = (EncryptionType)hdr->key_generation(); + mKeyIndex = (EncryptionKeyIndex)hdr->key_area_encryption_key_index(); mNcaSize = hdr->nca_size(); mProgramId = hdr->program_id(); - mUnk0 = hdr->unk0(); + mContentIndex = hdr->content_index(); + mSdkAddonVersion = hdr->sdk_addon_version(); for (size_t i = 0; i < kSectionNum; i++) { @@ -66,24 +74,81 @@ void NcaHeader::importBinary(const u8 * bytes, size_t len) // skip sections that don't exist if (hdr->section(section).start() == 0 && hdr->section(section).end() == 0) continue; + EncryptionType encType = mEncryptionType; + if (encType == CRYPT_AUTO) + { + if (mContentType == TYPE_PROGRAM && section == SECTION_LOGO) + { + encType = CRYPT_NONE; + } + else + { + encType = CRYPT_AESCTR; + } + } + // add high level struct - mSections.addElement({ blockNumToSize(hdr->section(section).start()), blockNumToSize(hdr->section(section).end() - hdr->section(section).start()), hdr->section(section).key_type(), hdr->section_hash(section) }); + mSections.addElement({ blockNumToSize(hdr->section(section).start()), blockNumToSize(hdr->section(section).end() - hdr->section(section).start()), encType, hdr->section_hash(section) }); } for (size_t i = 0; i < kAesKeyNum; i++) { - mAesKeys.addElement(hdr->aes_key(i)); + mEncAesKeys.addElement(hdr->enc_aes_key(i)); } } void nx::NcaHeader::clear() { - mBlockSize = 0; + mDistributionType = DIST_DOWNLOAD; + mContentType = TYPE_PROGRAM; + mEncryptionType = CRYPT_AUTO; + mKeyIndex = KEY_DEFAULT; mNcaSize = 0; mProgramId = 0; - mUnk0 = 0; + mContentIndex = 0; + mSdkAddonVersion = 0; mSections.clear(); - mAesKeys.clear(); + mEncAesKeys.clear(); +} + +nx::NcaHeader::DistributionType nx::NcaHeader::getDistributionType() const +{ + return mDistributionType; +} + +void nx::NcaHeader::setDistributionType(DistributionType type) +{ + mDistributionType = type; +} + +nx::NcaHeader::ContentType nx::NcaHeader::getContentType() const +{ + return mContentType; +} + +void nx::NcaHeader::setContentType(ContentType type) +{ + mContentType = type; +} + +nx::NcaHeader::EncryptionType nx::NcaHeader::getEncryptionType() const +{ + return mEncryptionType; +} + +void nx::NcaHeader::setEncryptionType(EncryptionType type) +{ + mEncryptionType = type; +} + +nx::NcaHeader::EncryptionKeyIndex nx::NcaHeader::getKeyIndex() const +{ + return mKeyIndex; +} + +void nx::NcaHeader::setKeyIndex(EncryptionKeyIndex index) +{ + mKeyIndex = index; } u64 NcaHeader::getNcaSize() const @@ -106,9 +171,24 @@ void NcaHeader::setProgramId(u64 program_id) mProgramId = program_id; } -u32 NcaHeader::getUnk() const +u32 nx::NcaHeader::getContentIndex() const { - return mUnk0; + return mContentIndex; +} + +void nx::NcaHeader::setContentIndex(u32 index) +{ + mContentIndex = index; +} + +u32 nx::NcaHeader::getSdkAddonVersion() const +{ + return mSdkAddonVersion; +} + +void nx::NcaHeader::setSdkAddonVersion(u32 version) +{ + mSdkAddonVersion = version; } const fnd::List& NcaHeader::getSections() const @@ -125,39 +205,43 @@ void NcaHeader::addSection(const sSection & section) mSections.addElement(section); } -const fnd::List& NcaHeader::getAesKeys() const +const fnd::List& NcaHeader::getEncAesKeys() const { - return mAesKeys; + return mEncAesKeys; } -void NcaHeader::addKey(const crypto::aes::sAes128Key & key) +void NcaHeader::addEncAesKey(const crypto::aes::sAes128Key & key) { - if (mAesKeys.getSize() >= kAesKeyNum) + if (mEncAesKeys.getSize() >= kAesKeyNum) { throw fnd::Exception(kModuleName, "Too many NCA aes keys"); } - mAesKeys.addElement(key); + mEncAesKeys.addElement(key); } u64 NcaHeader::blockNumToSize(u32 block_num) const { - return block_num*mBlockSize; + return block_num*kBlockSize; } u32 NcaHeader::sizeToBlockNum(u64 real_size) const { - return align(real_size, mBlockSize)/mBlockSize; + return align(real_size, kBlockSize)/kBlockSize; } bool NcaHeader::isEqual(const NcaHeader & other) const { - return (mBlockSize == other.mBlockSize) \ + return (mDistributionType == other.mDistributionType) \ + && (mContentType == other.mContentType) \ + && (mEncryptionType == other.mEncryptionType) \ + && (mKeyIndex == other.mKeyIndex) \ && (mNcaSize == other.mNcaSize) \ && (mProgramId == other.mProgramId) \ - && (mUnk0 == other.mUnk0) \ + && (mContentIndex == other.mContentIndex) \ + && (mSdkAddonVersion == other.mSdkAddonVersion) \ && (mSections == other.mSections) \ - && (mAesKeys == other.mAesKeys); + && (mEncAesKeys == other.mEncAesKeys); } void NcaHeader::copyFrom(const NcaHeader & other) @@ -168,13 +252,17 @@ void NcaHeader::copyFrom(const NcaHeader & other) } else { - this->mBinaryBlob.clear(); - mBlockSize = other.mBlockSize; + mBinaryBlob.clear(); + mDistributionType = other.mDistributionType; + mContentType = other.mContentType; + mEncryptionType = other.mEncryptionType; + mKeyIndex = other.mKeyIndex; mNcaSize = other.mNcaSize; mProgramId = other.mProgramId; - mUnk0 = other.mUnk0; + mContentIndex = other.mContentIndex; + mSdkAddonVersion = other.mSdkAddonVersion; mSections = other.mSections; - mAesKeys = other.mAesKeys; + mEncAesKeys = other.mEncAesKeys; } } diff --git a/lib/nx/NcaHeader.h b/lib/nx/NcaHeader.h index bc62fff..33c4ffd 100644 --- a/lib/nx/NcaHeader.h +++ b/lib/nx/NcaHeader.h @@ -12,18 +12,48 @@ namespace nx class NcaHeader : public ISerialiseableBinary { public: + enum DistributionType + { + DIST_DOWNLOAD, + DIST_GAME_CARD + }; + + enum ContentType + { + TYPE_PROGRAM, + TYPE_META, + TYPE_CONTROL, + TYPE_MANUAL, + TYPE_DATA, + }; + + enum EncryptionType + { + CRYPT_AUTO, + CRYPT_NONE, + CRYPT_AESCTR = 3 + }; + + enum EncryptionKeyIndex + { + KEY_UNUSED_0, + KEY_UNUSED_1, + KEY_DEFAULT, + KEY_UNUSED_3, + }; + struct sSection { u64 offset; u64 size; - u8 key_type; + EncryptionType enc_type; crypto::sha::sSha256Hash hash; const sSection& operator=(const sSection& other) { offset = other.offset; size = other.size; - key_type = other.key_type; + enc_type = other.enc_type; hash = other.hash; return *this; @@ -33,7 +63,7 @@ namespace nx { return (offset == other.offset) \ && (size == other.size) \ - && (key_type == other.key_type) \ + && (enc_type == other.enc_type) \ && (hash == other.hash); } @@ -43,7 +73,7 @@ namespace nx } }; - static const size_t kDefaultBlockSize = 0x200; + static const size_t kBlockSize = 0x200; NcaHeader(); NcaHeader(const NcaHeader& other); @@ -63,21 +93,40 @@ namespace nx // variables void clear(); + DistributionType getDistributionType() const; + void setDistributionType(DistributionType type); + ContentType getContentType() const; + void setContentType(ContentType type); + EncryptionType getEncryptionType() const; + void setEncryptionType(EncryptionType type); + EncryptionKeyIndex getKeyIndex() const; + void setKeyIndex(EncryptionKeyIndex index); u64 getNcaSize() const; void setNcaSize(u64 size); u64 getProgramId() const; void setProgramId(u64 program_id); - u32 getUnk() const; + u32 getContentIndex() const; + void setContentIndex(u32 index); + u32 getSdkAddonVersion() const; + void setSdkAddonVersion(u32 version); const fnd::List& getSections() const; void addSection(const sSection& section); - const fnd::List& getAesKeys() const; - void addKey(const crypto::aes::sAes128Key& key); + const fnd::List& getEncAesKeys() const; + void addEncAesKey(const crypto::aes::sAes128Key& key); private: const std::string kModuleName = "NCA_HEADER"; const std::string kNcaSig = "NCA2"; static const size_t kSectionNum = 4; static const size_t kAesKeyNum = 4; + static const u32 kDefaultSdkAddonVersion = 721920; + + enum ProgramPartitionId + { + SECTION_CODE, + SECTION_DATA, + SECTION_LOGO, + }; #pragma pack (push, 1) @@ -85,19 +134,21 @@ namespace nx { private: u8 signature_[4]; - u8 reserved_0[2]; - u16 block_size_; + u8 distribution_type_; + u8 content_type_; + u8 key_generation_; + u8 key_area_encryption_key_index_; u64 nca_size_; u64 program_id_; - u8 reserved_1[4]; - u32 unk_0_; + u32 content_index_; + u32 sdk_addon_version_; u8 reserved_2[0x20]; struct sNcaSection { private: u32 start_; // block units u32 end_; // block units - u8 key_type_; + u8 enabled_; u8 reserved[7]; public: u32 start() const { return le_word(start_); } @@ -106,17 +157,26 @@ namespace nx u32 end() const { return le_word(end_); } void set_end(u32 offset) { end_ = le_word(offset); } - u8 key_type() const { return key_type_; } - void set_key_type(u8 key_type) { key_type_ = key_type; } + u8 enabled() const { return enabled_; } + void set_enabled(u8 is_enabled) { enabled_ = is_enabled; } } section_[kSectionNum]; crypto::sha::sSha256Hash section_hash_[kSectionNum]; - crypto::aes::sAes128Key aes_keys_[kAesKeyNum]; + crypto::aes::sAes128Key enc_aes_keys_[kAesKeyNum]; public: const char* signature() const { return (const char*)signature_; } void set_signature(const char* signature) { memcpy(signature_, signature, 4); } - u16 block_size() const { return le_hword(block_size_); } - void set_block_size(u16 block_size) { block_size_ = le_hword(block_size); } + u8 distribution_type() const { return distribution_type_; } + void set_distribution_type(u8 type) { distribution_type_ = type; } + + u8 content_type() const { return content_type_; } + void set_content_type(u8 type) { content_type_ = type; } + + u8 key_generation() const { return key_generation_; } + void set_key_generation(u8 type) { key_generation_ = type; } + + u8 key_area_encryption_key_index() const { return key_area_encryption_key_index_; } + void set_key_area_encryption_key_index(u8 index) { key_area_encryption_key_index_ = index; } u64 nca_size() const { return le_dword(nca_size_); } void set_nca_size(u64 nca_size) { nca_size_ = le_dword(nca_size); } @@ -124,8 +184,11 @@ namespace nx u64 program_id() const { return le_dword(program_id_); } void set_program_id(u64 program_id) { program_id_ = le_dword(program_id); } - u32 unk0() const { return le_word(unk_0_); } - void set_unk0(u32 val) { unk_0_ = le_word(val); } + u32 content_index() const { return le_word(content_index_); } + void set_content_index(u32 index) { content_index_ = le_word(index); } + + u32 sdk_addon_version() const { return le_word(sdk_addon_version_); } + void set_sdk_addon_version(u32 version) { sdk_addon_version_ = le_word(version); } const sNcaSection& section(u8 index) const { return section_[index%kSectionNum]; } sNcaSection& section(u8 index) { return section_[index%kSectionNum]; } @@ -133,8 +196,8 @@ namespace nx const crypto::sha::sSha256Hash& section_hash(u8 index) const { return section_hash_[index%kSectionNum]; } crypto::sha::sSha256Hash& section_hash(u8 index) { return section_hash_[index%kSectionNum]; } - const crypto::aes::sAes128Key& aes_key(u8 index) const { return aes_keys_[index%kAesKeyNum]; } - crypto::aes::sAes128Key& aes_key(u8 index) { return aes_keys_[index%kAesKeyNum]; } + const crypto::aes::sAes128Key& enc_aes_key(u8 index) const { return enc_aes_keys_[index%kAesKeyNum]; } + crypto::aes::sAes128Key& enc_aes_key(u8 index) { return enc_aes_keys_[index%kAesKeyNum]; } }; #pragma pack (pop) @@ -142,12 +205,16 @@ namespace nx fnd::MemoryBlob mBinaryBlob; // data - u16 mBlockSize; + DistributionType mDistributionType; + ContentType mContentType; + EncryptionType mEncryptionType; + EncryptionKeyIndex mKeyIndex; u64 mNcaSize; u64 mProgramId; - u32 mUnk0; + u32 mContentIndex; + u32 mSdkAddonVersion; fnd::List mSections; - fnd::List mAesKeys; + fnd::List mEncAesKeys; u64 blockNumToSize(u32 block_num) const; u32 sizeToBlockNum(u64 real_size) const; diff --git a/lib/nx/NpdmHeader.h b/lib/nx/NpdmHeader.h index 588fb83..ad630b5 100644 --- a/lib/nx/NpdmHeader.h +++ b/lib/nx/NpdmHeader.h @@ -10,7 +10,6 @@ namespace nx public nx::ISerialiseableBinary { public: - // move these enums to NpdmBinary? enum InstructionType { INSTR_32BIT, diff --git a/lib/nx/PfsHeader.cpp b/lib/nx/PfsHeader.cpp new file mode 100644 index 0000000..205a26f --- /dev/null +++ b/lib/nx/PfsHeader.cpp @@ -0,0 +1,170 @@ +#include "PfsHeader.h" + + + +nx::PfsHeader::PfsHeader() +{} + +nx::PfsHeader::PfsHeader(const PfsHeader & other) +{ + copyFrom(other); +} + +nx::PfsHeader::PfsHeader(const u8 * bytes, size_t len) +{ + importBinary(bytes, len); +} + +void nx::PfsHeader::exportBinary() +{ + // calculate name table size + size_t name_table_size = 0; + for (size_t i = 0; i < mFileList.getSize(); i++) + { + name_table_size += mFileList[i].name.length() + 1; + } + + size_t pfs_header_size = align(sizeof(sPfsHeader) + sizeof(sPfsFile) * mFileList.getSize() + name_table_size, kPfsAlign); + + // align name_table_size + name_table_size = pfs_header_size - (sizeof(sPfsHeader) + sizeof(sPfsFile) * mFileList.getSize()); + + // allocate pfs header binary + mBinaryBlob.alloc(pfs_header_size); + sPfsHeader* hdr = (sPfsHeader*)mBinaryBlob.getBytes(); + + // set header fields + hdr->set_signature(kPfsStructSig.c_str()); + hdr->set_file_num(mFileList.getSize()); + hdr->set_name_table_size(name_table_size); + + // set file entries + sPfsFile* raw_files = (sPfsFile*)(mBinaryBlob.getBytes() + sizeof(sPfsHeader)); + char* raw_name_table = (char*)(mBinaryBlob.getBytes() + sizeof(sPfsHeader) + sizeof(sPfsFile) * mFileList.getSize()); + size_t raw_name_table_pos = 0; + + calculateOffsets(pfs_header_size); + for (size_t i = 0; i < mFileList.getSize(); i++) + { + raw_files[i].set_offset(mFileList[i].offset - pfs_header_size); + raw_files[i].set_size(mFileList[i].size); + raw_files[i].set_name_offset(raw_name_table_pos); + + strcpy(raw_name_table + raw_name_table_pos, mFileList[i].name.c_str()); + raw_name_table_pos += mFileList[i].name.length() + 1; + } +} + +void nx::PfsHeader::importBinary(const u8 * bytes, size_t len) +{ + // check input length meets minimum size + if (len < sizeof(sPfsHeader)) + { + throw fnd::Exception(kModuleName, "PFS header too small"); + } + + // import minimum header + mBinaryBlob.alloc(sizeof(sPfsHeader)); + memcpy(mBinaryBlob.getBytes(), bytes, mBinaryBlob.getSize()); + const sPfsHeader* hdr = (const sPfsHeader*)mBinaryBlob.getBytes(); + + // check struct signature + if (memcmp(hdr->signature(), kPfsStructSig.c_str(), 4) != 0) + { + throw fnd::Exception(kModuleName, "PFS header corrupt"); + } + + // determine complete header size + size_t pfs_full_header_size = sizeof(sPfsHeader) + sizeof(sPfsFile) * hdr->file_num() + hdr->name_table_size(); + + // check input length meets complete size + if (len < pfs_full_header_size) + { + throw fnd::Exception(kModuleName, "PFS header too small"); + } + + // import full header + mBinaryBlob.alloc(pfs_full_header_size); + memcpy(mBinaryBlob.getBytes(), bytes, mBinaryBlob.getSize()); + hdr = (const sPfsHeader*)mBinaryBlob.getBytes(); + + // clear variables + clear(); + + // get pointers to raw data + const sPfsFile* raw_files = (const sPfsFile*)(mBinaryBlob.getBytes() + sizeof(sPfsHeader)); + const char* raw_name_table = (const char*)(mBinaryBlob.getBytes() + sizeof(sPfsHeader) + sizeof(sPfsFile) * hdr->file_num()); + + // process file entries + for (size_t i = 0; i < hdr->file_num(); i++) + { + mFileList.addElement({ std::string(raw_name_table + raw_files[i].name_offset()), raw_files[i].offset() + pfs_full_header_size, raw_files[i].size() }); + } +} + +void nx::PfsHeader::clear() +{ + mBinaryBlob.clear(); + mFileList.clear(); +} + +const fnd::List& nx::PfsHeader::getFileList() const +{ + return mFileList; +} + +void nx::PfsHeader::addFile(const std::string & name, size_t size) +{ + mFileList.addElement({ name, 0, size }); +} + +void nx::PfsHeader::calculateOffsets(size_t data_offset) +{ + for (size_t i = 0; i < mFileList.getSize(); i++) + { + mFileList[i].offset = (i == 0) ? data_offset : mFileList[i - 1].offset + mFileList[i - 1].size; + } +} + +bool nx::PfsHeader::isEqual(const PfsHeader & other) const +{ + return mFileList == other.mFileList; +} + +void nx::PfsHeader::copyFrom(const PfsHeader & other) +{ + if (other.getSize()) + { + importBinary(other.getBytes(), other.getSize()); + } + else + { + clear(); + mFileList = other.mFileList; + } +} + +bool nx::PfsHeader::operator==(const PfsHeader & other) const +{ + return isEqual(other); +} + +bool nx::PfsHeader::operator!=(const PfsHeader & other) const +{ + return !isEqual(other); +} + +void nx::PfsHeader::operator=(const PfsHeader & other) +{ + copyFrom(other); +} + +const u8 * nx::PfsHeader::getBytes() const +{ + return mBinaryBlob.getBytes(); +} + +size_t nx::PfsHeader::getSize() const +{ + return mBinaryBlob.getSize(); +} diff --git a/lib/nx/PfsHeader.h b/lib/nx/PfsHeader.h new file mode 100644 index 0000000..fbd8a57 --- /dev/null +++ b/lib/nx/PfsHeader.h @@ -0,0 +1,126 @@ +#pragma once +#include +#include +#include +#include +#include + + +namespace nx +{ + class PfsHeader : + public ISerialiseableBinary + { + public: + struct sFile + { + std::string name; + size_t offset; + size_t size; + + sFile& operator=(const sFile& other) + { + name = other.name; + offset = other.offset; + size = other.size; + return *this; + } + + bool operator==(const sFile& other) const + { + return (name == other.name) \ + && (offset == other.offset) \ + && (size == other.size); + } + + bool operator!=(const sFile& other) const + { + return !operator==(other); + } + + bool operator==(const std::string& other) const + { + return (name == other); + } + + bool operator!=(const std::string& other) const + { + return !operator==(other); + } + }; + + PfsHeader(); + PfsHeader(const PfsHeader& other); + PfsHeader(const u8* bytes, size_t len); + + bool operator==(const PfsHeader& other) const; + bool operator!=(const PfsHeader& other) const; + void operator=(const PfsHeader& other); + + // to be used after export + const u8* getBytes() const; + size_t getSize() const; + + // export/import binary + void exportBinary(); + void importBinary(const u8* bytes, size_t len); + + // variables + void clear(); + + const fnd::List& getFileList() const; + void addFile(const std::string& name, size_t size); + + private: + const std::string kModuleName = "PFS_HEADER"; + const std::string kPfsStructSig = "PFS0"; + static const size_t kPfsAlign = 0x40; + +#pragma pack (push, 1) + struct sPfsFile + { + private: + u64 data_offset_; + u64 size_; + u64 name_offset_; + public: + u64 offset() const { return le_dword(data_offset_); } + void set_offset(u64 offset) { data_offset_ = le_dword(offset); } + + u64 size() const { return le_dword(size_); } + void set_size(u64 size) { size_ = le_dword(size); } + + u64 name_offset() const { return le_dword(name_offset_); } + void set_name_offset(u64 offset) { name_offset_ = le_dword(offset); } + }; + + struct sPfsHeader + { + private: + u8 signature_[4]; + u32 file_num_; + u64 name_table_size_; + public: + const char* signature() const { return (const char*)signature_; } + void set_signature(const char* signature) { memcpy(signature_, signature, 4); } + + u32 file_num() const { return le_word(file_num_); } + void set_file_num(u32 file_num) { file_num_ = le_word(file_num); } + + u64 name_table_size() const { return le_dword(name_table_size_); } + void set_name_table_size(u64 size) { name_table_size_ = le_dword(size); } + }; +#pragma pack (pop) + + // binary blob + fnd::MemoryBlob mBinaryBlob; + + // variables + fnd::List mFileList; + + void calculateOffsets(size_t data_offset); + bool isEqual(const PfsHeader& other) const; + void copyFrom(const PfsHeader& other); + }; +} + diff --git a/lib/nx/XciHeader.cpp b/lib/nx/XciHeader.cpp new file mode 100644 index 0000000..e9f52c4 --- /dev/null +++ b/lib/nx/XciHeader.cpp @@ -0,0 +1,6 @@ +#include "XciHeader.h" + + + +nx::XciHeader::XciHeader() +{} diff --git a/lib/nx/XciHeader.h b/lib/nx/XciHeader.h new file mode 100644 index 0000000..1370a4e --- /dev/null +++ b/lib/nx/XciHeader.h @@ -0,0 +1,92 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace nx +{ + class XciHeader// : + //public ISerialiseableBinary + { + public: + XciHeader(); + + private: +#pragma pack (push, 1) + enum ContentMetaType + { + SYSTEM_PROGRAM = 1, + SYSTEM_DATA = 2, + SYSTEM_UPDATE = 3, + BOOT_IMAGE_PACKAGE = 4, + BOOT_IMAGE_PACKAGE_SAFE = 5, + APPLICATION = 128, + PATCH = 129, + ADD_ON_CONTENT = 130 + }; + + enum ContentType + { + META, + PROGRAM, + DATA, + CONTROL, + HTML_DOCUMENT, + LEGAL_INFORMATION + }; + + enum ContentMetaAttribute + { + None = 0, + IncludesExFatDriver = 1 + }; + + struct sContentMetaInfo + { + u64 id; + u32 version; + u8 type; // ContentMetaType + u8 attributes; + u8 reserved[2]; + }; + + struct sContentInfo + { + u8 id[16]; + u32 size_low; + u16 size_high; + u8 type; + u8 reserved; + }; + + struct sXciHeader + { + u8 signature[4]; + u32 rom_area_start_page; + u32 backup_area_start_page; + u8 key_flag; // bit0-3 = KekIndex, bit4-7 = TitleKeyDecIndex + u8 rom_size; // this is an enum + u8 flags; + u8 package_id[8]; // stylised as 0x{0:x2}{1:x2}{2:x2}{3:x2}_{4:x2}{5:x2}{6:x2}{7:x2} + u32 valid_data_end_page; + u8 reserved_0[100]; + u32 sel_sec; + u32 sel_t1_key; + u32 sel_key; + u32 lim_area; + u32 fw_version[2]; // [0]=minor, [1]=major + u32 acc_ctrl_1; + u8 reserved_1[0x10]; + u32 fw_mode; + u32 cup_version; + u8 reserved_2[0x4]; + u8 upp_hash[8]; // stylised as 0x{0:x2}{1:x2}{2:x2}{3:x2}_{4:x2}{5:x2}{6:x2}{7:x2} + u64 cup_id; // cup programID? + + }; +#pragma pack (pop) + }; + +} diff --git a/lib/nx/nx.vcxproj b/lib/nx/nx.vcxproj index 5d75860..b8ee91b 100644 --- a/lib/nx/nx.vcxproj +++ b/lib/nx/nx.vcxproj @@ -44,12 +44,14 @@ + + @@ -74,12 +76,14 @@ + + 15.0 diff --git a/lib/nx/nx.vcxproj.filters b/lib/nx/nx.vcxproj.filters index 0785a9e..c3e4800 100644 --- a/lib/nx/nx.vcxproj.filters +++ b/lib/nx/nx.vcxproj.filters @@ -108,6 +108,12 @@ Header Files + + Header Files + + + Header Files + @@ -194,5 +200,11 @@ Source Files + + Source Files + + + Source Files + \ No newline at end of file diff --git a/programs/makefile b/programs/makefile index d91fc90..564d602 100644 --- a/programs/makefile +++ b/programs/makefile @@ -1,4 +1,4 @@ -PROGS = ncatool npdmtool +PROGS = ncatool npdmtool pfstool main: build diff --git a/programs/ncatool/main.cpp b/programs/ncatool/main.cpp index 68f3f45..ca13323 100644 --- a/programs/ncatool/main.cpp +++ b/programs/ncatool/main.cpp @@ -5,8 +5,14 @@ #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + +const size_t kNcaSectorSize = nx::NcaHeader::kBlockSize; -const size_t kNcaSectorSize = nx::NcaHeader::kDefaultBlockSize; void initNcaCtr(u8 ctr[crypto::aes::kAesBlockSize], u32 generation) { @@ -25,6 +31,14 @@ void hexDump(const u8* data, size_t len) } } +void xorData(const u8* a, const u8* b, u8* out, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + out[i] = a[i] ^ b[i]; + } +} + void decryptNcaSectorXts(const fnd::MemoryBlob& nca, u8 out[kNcaSectorSize], size_t sector, const u8* key1, const u8* key2) { u8 tweak[crypto::aes::kAesBlockSize]; @@ -49,6 +63,63 @@ void dumpNcaSector(u8 out[kNcaSectorSize]) } } +void dumpHxdStyleSector(u8* out, size_t len) +{ + // iterate over 0x10 blocks + for (size_t i = 0; i < (len / crypto::aes::kAesBlockSize); i++) + { + // for block i print each byte + for (size_t j = 0; j < crypto::aes::kAesBlockSize; j++) + { + printf("%02X ", out[i*crypto::aes::kAesBlockSize + j]); + } + printf(" "); + for (size_t j = 0; j < crypto::aes::kAesBlockSize; j++) + { + printf("%c", isalnum(out[i*crypto::aes::kAesBlockSize + j]) ? out[i*crypto::aes::kAesBlockSize + j] : '.'); + } + printf("\n"); + } + + /* + for (size_t i = 0; i < len % crypto::aes::kAesBlockSize; i++) + { + printf("%02X ", out[(len / crypto::aes::kAesBlockSize)*crypto::aes::kAesBlockSize + i]); + } + for (size_t i = 0; i < crypto::aes::kAesBlockSize - (len % crypto::aes::kAesBlockSize); i++) + { + printf(" "); + } + for (size_t i = 0; i < len % crypto::aes::kAesBlockSize; i++) + { + printf("%c", out[(len / crypto::aes::kAesBlockSize)*crypto::aes::kAesBlockSize + i]); + } + */ +} + +std::string kDistributionTypeStr[] +{ + "Download", + "Game Card" +}; + +std::string kContentTypeStr[] +{ + "Program", + "Meta", + "Control", + "Manual", + "Data" +}; + +std::string kEncryptionTypeStr[] +{ + "Auto", + "None", + "UNKNOWN_2", + "AesCtr" +}; + int main(int argc, char** argv) { if (argc < 2) @@ -65,7 +136,7 @@ int main(int argc, char** argv) u8 sector[kNcaSectorSize]; // nca test - if (argc == 2) + if (argc == 2 || argc == 3) { decryptNcaSectorXts(nca, sector, 1, crypto::aes::nx::nca_header_key[0], crypto::aes::nx::nca_header_key[1]); @@ -73,9 +144,14 @@ int main(int argc, char** argv) hdr.importBinary(sector, kNcaSectorSize); printf("[NCA Header]\n"); - printf(" Size: 0x%" PRIx64 "\n", hdr.getNcaSize()); - printf(" ProgID: 0x%016" PRIx64 "\n", hdr.getProgramId()); - printf(" Unk0: 0x%" PRIx32 "\n", hdr.getUnk()); + printf(" Dist. Type: %s\n", kDistributionTypeStr[hdr.getDistributionType()].c_str()); + printf(" Type: %s\n", kContentTypeStr[hdr.getContentType()].c_str()); + printf(" Enc. Type: %s\n", kEncryptionTypeStr[hdr.getEncryptionType()].c_str()); + printf(" KeyIndex: %d\n", hdr.getKeyIndex()); + printf(" Size: 0x%" PRIx64 "\n", hdr.getNcaSize()); + printf(" ProgID: 0x%016" PRIx64 "\n", hdr.getProgramId()); + printf(" Content. Idx: %" PRIu32 "\n", hdr.getContentIndex()); + printf(" SdkAddon Ver.: v%" PRIu32 "\n", hdr.getSdkAddonVersion()); printf(" Sections:\n"); for (size_t i = 0; i < hdr.getSections().getSize(); i++) { @@ -83,23 +159,41 @@ int main(int argc, char** argv) printf(" %lu:\n", i); //printf(" Start Blk: %" PRId32 "\n", section.start_blk); //printf(" End Blk: %" PRId32 "\n", section.end_blk); - printf(" Offset: 0x%" PRIx64 "\n", section.offset); - printf(" Size: 0x%" PRIx64 "\n", section.size); - printf(" KeyType: 0x%02x\n", section.key_type); - printf(" Hash: "); + printf(" Offset: 0x%" PRIx64 "\n", section.offset); + printf(" Size: 0x%" PRIx64 "\n", section.size); + printf(" Enc. Type: %s\n", kEncryptionTypeStr[section.enc_type].c_str()); + printf(" Hash: "); hexDump(section.hash.bytes, crypto::sha::kSha256HashLen); printf("\n"); } - printf(" AES Keys:\n"); - for (size_t i = 0; i < hdr.getAesKeys().getSize(); i++) + printf(" Encrypted Body Keys:\n"); + for (size_t i = 0; i < hdr.getEncAesKeys().getSize(); i++) { printf(" %lu: ", i); - hexDump(hdr.getAesKeys()[i].key, crypto::aes::kAes128KeySize); + hexDump(hdr.getEncAesKeys()[i].key, crypto::aes::kAes128KeySize); printf("\n"); } + if (argc == 3) + { +#ifdef _WIN32 + _mkdir(argv[2]); +#else + mkdir(argv[2], S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); +#endif + for (size_t i = 0; i < hdr.getSections().getSize(); i++) + { + const nx::NcaHeader::sSection& section = hdr.getSections()[i]; +#ifdef _WIN32 + fnd::io::writeFile(std::string(argv[2]) + "\\" + std::to_string(i) + ".bin" , nca.getBytes() + section.offset, section.size); +#else + fnd::io::writeFile(std::string(argv[2]) + "/" + std::to_string(i) + ".bin", nca.getBytes() + section.offset, section.size); +#endif + } + } } + } catch (const fnd::Exception& e) { printf("%s\n",e.what()); diff --git a/programs/ncatool/ncatool.vcxproj b/programs/ncatool/ncatool.vcxproj index 18e0c53..3b70f76 100644 --- a/programs/ncatool/ncatool.vcxproj +++ b/programs/ncatool/ncatool.vcxproj @@ -116,6 +116,9 @@ + + + diff --git a/programs/ncatool/ncatool.vcxproj.filters b/programs/ncatool/ncatool.vcxproj.filters index 0d8d9e4..61ac64b 100644 --- a/programs/ncatool/ncatool.vcxproj.filters +++ b/programs/ncatool/ncatool.vcxproj.filters @@ -19,4 +19,7 @@ Source Files + + + \ No newline at end of file diff --git a/programs/pfstool/main.cpp b/programs/pfstool/main.cpp new file mode 100644 index 0000000..6d9a12f --- /dev/null +++ b/programs/pfstool/main.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +int main(int argc, char** argv) +{ + if (argc < 2) + { + printf("usage: pfstool []\n"); + return 1; + } + + try + { + fnd::MemoryBlob file; + fnd::io::readFile(argv[1], file); + + // import + nx::PfsHeader pfs; + pfs.importBinary(file.getBytes(), file.getSize()); + + if (argc == 3) + { +#ifdef _WIN32 + _mkdir(argv[2]); +#else + mkdir(argv[2], S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); +#endif + } + + printf("[PFS]\n"); + for (size_t i = 0; i < pfs.getFileList().getSize(); i++) + { + printf(" %s (offset=0x%" PRIx64 ", size=0x%" PRIx64 ")\n", pfs.getFileList()[i].name.c_str(), pfs.getFileList()[i].offset, pfs.getFileList()[i].size); + if (argc == 3) + { +#ifdef _WIN32 + fnd::io::writeFile(std::string(argv[2]) + "\\" + pfs.getFileList()[i].name, file.getBytes() + pfs.getFileList()[i].offset, pfs.getFileList()[i].size); +#else + fnd::io::writeFile(std::string(argv[2]) + "/" + pfs.getFileList()[i].name, file.getBytes() + pfs.getFileList()[i].offset, pfs.getFileList()[i].size); +#endif + } + + } + + + } catch (const fnd::Exception& e) + { + printf("%s\n", e.what()); + } + + return 0; +} \ No newline at end of file diff --git a/programs/pfstool/makefile b/programs/pfstool/makefile new file mode 100644 index 0000000..117d52e --- /dev/null +++ b/programs/pfstool/makefile @@ -0,0 +1,39 @@ +# Sources +SRC_DIR = . +OBJS = $(foreach dir,$(SRC_DIR),$(subst .cpp,.o,$(wildcard $(dir)/*.cpp))) $(foreach dir,$(SRC_DIR),$(subst .c,.o,$(wildcard $(dir)/*.c))) + +#local dependencies +DEPENDS = nx crypto fnd + +LIB_DIR = ../../lib + +LIBS = -L"$(LIB_DIR)" $(foreach dep,$(DEPENDS), -l"$(dep)") +INCS = -I"$(LIB_DIR)/" + +OUTPUT = ../../bin/$(shell basename $(CURDIR)) + +# Compiler Settings +CXXFLAGS = -std=c++11 $(INCS) -D__STDC_FORMAT_MACROS -Wall -Wno-unused-but-set-variable -Wno-unused-value +ifeq ($(OS),Windows_NT) + # Windows Only Flags/Libs + CC = x86_64-w64-mingw32-gcc + CXX = x86_64-w64-mingw32-g++ + CFLAGS += + CXXFLAGS += + LIBS += -static +else + # *nix Only Flags/Libs + CFLAGS += + CXXFLAGS += + LIBS += +endif + +all: build + +rebuild: clean build + +build: $(OBJS) + $(CXX) $(OBJS) $(LIBS) -o $(OUTPUT) + +clean: + rm -rf $(OBJS) $(OUTPUT) \ No newline at end of file diff --git a/programs/pfstool/pfstool.vcxproj b/programs/pfstool/pfstool.vcxproj new file mode 100644 index 0000000..7e15c82 --- /dev/null +++ b/programs/pfstool/pfstool.vcxproj @@ -0,0 +1,125 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {BC2F2D07-BAB3-469C-9C25-8CC54F96F7AB} + pfstool + 8.1 + + + + Application + true + v140 + MultiByte + + + Application + false + v140 + true + MultiByte + + + Application + true + v140 + MultiByte + + + Application + false + v140 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + true + ..\..\lib + + + + + Level3 + Disabled + true + ..\..\lib + + + + + Level3 + MaxSpeed + true + true + true + ..\..\lib + + + true + true + + + + + Level3 + MaxSpeed + true + true + true + ..\..\lib + + + true + true + + + + + + + + + + + + \ No newline at end of file diff --git a/programs/pfstool/pfstool.vcxproj.filters b/programs/pfstool/pfstool.vcxproj.filters new file mode 100644 index 0000000..61ac64b --- /dev/null +++ b/programs/pfstool/pfstool.vcxproj.filters @@ -0,0 +1,25 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + + + + \ No newline at end of file