feat: Add support for encrypted NCSD

Missing version 1 support and seeded crypto but that would be enough for now.
This commit is contained in:
Mary 2023-06-19 23:13:38 +02:00
parent 2e5bc0cb14
commit 86dd7f54f5
8 changed files with 305 additions and 138 deletions

View file

@ -143,7 +143,7 @@ source_group("Source Files\\Third Party" FILES ${THIRD_PARTY_SOURCE_FILES})
add_executable(Alber ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} add_executable(Alber ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES}
${PICA_SOURCE_FILES} ${RENDERER_GL_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${HEADER_FILES}) ${PICA_SOURCE_FILES} ${RENDERER_GL_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${HEADER_FILES})
target_link_libraries(Alber PRIVATE dynarmic SDL2-static glad) target_link_libraries(Alber PRIVATE dynarmic SDL2-static glad cryptopp)
if(GPU_DEBUG_INFO) if(GPU_DEBUG_INFO)

View file

@ -1,14 +1,22 @@
#pragma once #pragma once
#include <array> #include <array>
#include <optional>
#include <vector> #include <vector>
#include "io_file.hpp" #include "io_file.hpp"
#include "helpers.hpp" #include "helpers.hpp"
#include "crypto/aes_engine.hpp"
struct NCCH { struct NCCH {
struct EncryptionInfo {
Crypto::AESKey normalKey;
Crypto::AESKey initialCounter;
};
struct FSInfo { // Info on the ExeFS/RomFS struct FSInfo { // Info on the ExeFS/RomFS
u64 offset = 0; u64 offset = 0;
u64 size = 0; u64 size = 0;
u64 hashRegionSize = 0; u64 hashRegionSize = 0;
std::optional<EncryptionInfo> encryptionInfo;
}; };
// Descriptions for .text, .data and .rodata sections // Descriptions for .text, .data and .rodata sections
@ -34,6 +42,8 @@ struct NCCH {
bool mountRomFS = false; bool mountRomFS = false;
bool encrypted = false; bool encrypted = false;
bool fixedCryptoKey = false; bool fixedCryptoKey = false;
bool seedCrypto = false;
u8 secondaryKeySlot = 0;
static constexpr u64 mediaUnit = 0x200; static constexpr u64 mediaUnit = 0x200;
u64 size = 0; // Size of NCCH converted to bytes u64 size = 0; // Size of NCCH converted to bytes
@ -41,6 +51,7 @@ struct NCCH {
u32 bssSize = 0; u32 bssSize = 0;
u32 exheaderSize = 0; u32 exheaderSize = 0;
FSInfo exheaderInfo;
FSInfo exeFS; FSInfo exeFS;
FSInfo romFS; FSInfo romFS;
CodeSetInfo text, data, rodata; CodeSetInfo text, data, rodata;
@ -50,10 +61,9 @@ struct NCCH {
// Contains of the cart's save data // Contains of the cart's save data
std::vector<u8> saveData; std::vector<u8> saveData;
// Header: 0x200 + 0x800 byte NCCH header + exheadr
// Returns true on success, false on failure // Returns true on success, false on failure
// Partition index/offset/size must have been set before this // Partition index/offset/size must have been set before this
bool loadFromHeader(u8* header, IOFile& file); bool loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info);
bool hasExtendedHeader() { return exheaderSize != 0; } bool hasExtendedHeader() { return exheaderSize != 0; }
bool hasExeFS() { return exeFS.size != 0; } bool hasExeFS() { return exeFS.size != 0; }
@ -61,7 +71,8 @@ struct NCCH {
bool hasCode() { return codeFile.size() != 0; } bool hasCode() { return codeFile.size() != 0; }
bool hasSaveData() { return saveData.size() != 0; } bool hasSaveData() { return saveData.size() != 0; }
private: std::pair<bool, Crypto::AESKey> getPrimaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY);
std::array<u8, 16> primaryKey = {}; // For exheader, ExeFS header and icons std::pair<bool, Crypto::AESKey> getSecondaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY);
std::array<u8, 16> secondaryKey = {}; // For RomFS and some files in ExeFS
std::pair<bool, std::size_t> readFromFile(IOFile& file, const FSInfo &info, u8 *dst, std::size_t offset, std::size_t size);
}; };

View file

@ -5,6 +5,7 @@
#include <fstream> #include <fstream>
#include <optional> #include <optional>
#include <vector> #include <vector>
#include "crypto/aes_engine.hpp"
#include "helpers.hpp" #include "helpers.hpp"
#include "handles.hpp" #include "handles.hpp"
#include "loader/ncsd.hpp" #include "loader/ncsd.hpp"
@ -146,7 +147,7 @@ public:
void* getReadPointer(u32 address); void* getReadPointer(u32 address);
void* getWritePointer(u32 address); void* getWritePointer(u32 address);
std::optional<u32> loadELF(std::ifstream& file); std::optional<u32> loadELF(std::ifstream& file);
std::optional<NCSD> loadNCSD(const std::filesystem::path& path); std::optional<NCSD> loadNCSD(Crypto::AESEngine &aesEngine, const std::filesystem::path& path);
u8 read8(u32 vaddr); u8 read8(u32 vaddr);
u16 read16(u32 vaddr); u16 read16(u32 vaddr);

View file

@ -133,6 +133,8 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
auto cxi = mem.getCXI(); auto cxi = mem.getCXI();
IOFile& ioFile = mem.CXIFile; IOFile& ioFile = mem.CXIFile;
NCCH::FSInfo fsInfo;
// Seek to file offset depending on if we're reading from RomFS, ExeFS, etc // Seek to file offset depending on if we're reading from RomFS, ExeFS, etc
switch (type) { switch (type) {
case PathType::RomFS: { case PathType::RomFS: {
@ -142,9 +144,8 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
Helpers::panic("Tried to read from NCCH with too big of an offset"); Helpers::panic("Tried to read from NCCH with too big of an offset");
} }
if (!ioFile.seek(cxi->fileOffset + romFSOffset + offset + 0x1000)) { fsInfo = cxi->romFS;
Helpers::panic("Failed to seek while reading from RomFS"); offset += 0x1000;
}
break; break;
} }
@ -153,7 +154,7 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
} }
std::unique_ptr<u8[]> data(new u8[size]); std::unique_ptr<u8[]> data(new u8[size]);
auto [success, bytesRead] = ioFile.readBytes(&data[0], size); auto [success, bytesRead] = cxi->readFromFile(ioFile, fsInfo, &data[0], offset, size);
if (!success) { if (!success) {
Helpers::panic("Failed to read from NCCH archive"); Helpers::panic("Failed to read from NCCH archive");

View file

@ -71,6 +71,8 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
auto cxi = mem.getCXI(); auto cxi = mem.getCXI();
IOFile& ioFile = mem.CXIFile; IOFile& ioFile = mem.CXIFile;
NCCH::FSInfo fsInfo;
// Seek to file offset depending on if we're reading from RomFS, ExeFS, etc // Seek to file offset depending on if we're reading from RomFS, ExeFS, etc
switch (type) { switch (type) {
case PathType::RomFS: { case PathType::RomFS: {
@ -80,9 +82,8 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
Helpers::panic("Tried to read from SelfNCCH with too big of an offset"); Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
} }
if (!ioFile.seek(cxi->fileOffset + romFSOffset + offset + 0x1000)) { fsInfo = cxi->romFS;
Helpers::panic("Failed to seek while reading from RomFS"); offset += 0x1000;
}
break; break;
} }
@ -93,9 +94,7 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
Helpers::panic("Tried to read from SelfNCCH with too big of an offset"); Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
} }
if (!ioFile.seek(cxi->fileOffset + exeFSOffset + offset)) { // TODO: Not sure if this needs the + 0x1000 fsInfo = cxi->exeFS;
Helpers::panic("Failed to seek while reading from ExeFS");
}
break; break;
} }
@ -104,7 +103,7 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
} }
std::unique_ptr<u8[]> data(new u8[size]); std::unique_ptr<u8[]> data(new u8[size]);
auto [success, bytesRead] = ioFile.readBytes(&data[0], size); auto [success, bytesRead] = cxi->readFromFile(ioFile, fsInfo, &data[0], offset, size);
if (!success) { if (!success) {
Helpers::panic("Failed to read from SelfNCCH archive"); Helpers::panic("Failed to read from SelfNCCH archive");

View file

@ -1,10 +1,24 @@
#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cstring> #include <cstring>
#include <vector> #include <vector>
#include "loader/lz77.hpp" #include "loader/lz77.hpp"
#include "loader/ncch.hpp" #include "loader/ncch.hpp"
#include "memory.hpp" #include "memory.hpp"
bool NCCH::loadFromHeader(u8* header, IOFile& file) { #include <iostream>
bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info) {
// 0x200 bytes for the NCCH header
constexpr u64 headerSize = 0x200;
u8 header[headerSize];
auto [success, bytes] = readFromFile(file, info, header, 0, headerSize);
if (!success || bytes != headerSize) {
printf("Failed to read NCCH header\n");
return false;
}
if (header[0x100] != 'N' || header[0x101] != 'C' || header[0x102] != 'C' || header[0x103] != 'H') { if (header[0x100] != 'N' || header[0x101] != 'C' || header[0x102] != 'C' || header[0x103] != 'H') {
printf("Invalid header on NCCH\n"); printf("Invalid header on NCCH\n");
return false; return false;
@ -19,26 +33,83 @@ bool NCCH::loadFromHeader(u8* header, IOFile& file) {
const u64 programID = *(u64*)&header[0x118]; const u64 programID = *(u64*)&header[0x118];
// Read NCCH flags // Read NCCH flags
secondaryKeySlot = header[0x188 + 3];
isNew3DS = header[0x188 + 4] == 2; isNew3DS = header[0x188 + 4] == 2;
fixedCryptoKey = (header[0x188 + 7] & 0x1) == 0x1; fixedCryptoKey = (header[0x188 + 7] & 0x1) == 0x1;
mountRomFS = (header[0x188 + 7] & 0x2) != 0x2; mountRomFS = (header[0x188 + 7] & 0x2) != 0x2;
encrypted = (header[0x188 + 7] & 0x4) != 0x4; encrypted = (header[0x188 + 7] & 0x4) != 0x4;
seedCrypto = (header[0x188 + 7] & 0x20) == 0x20;
// Read ExeFS and RomFS info // Read exheader, ExeFS and RomFS info
exeFS.offset = u64(*(u32*)&header[0x1A0]) * mediaUnit; exheaderInfo.offset = info.offset + 0x200;
exheaderInfo.size = exheaderSize;
exheaderInfo.hashRegionSize = 0;
exeFS.offset = info.offset + u64(*(u32*)&header[0x1A0]) * mediaUnit;
exeFS.size = u64(*(u32*)&header[0x1A4]) * mediaUnit; exeFS.size = u64(*(u32*)&header[0x1A4]) * mediaUnit;
exeFS.hashRegionSize = u64(*(u32*)&header[0x1A8]) * mediaUnit; exeFS.hashRegionSize = u64(*(u32*)&header[0x1A8]) * mediaUnit;
romFS.offset = u64(*(u32*)&header[0x1B0]) * mediaUnit; romFS.offset = info.offset + u64(*(u32*)&header[0x1B0]) * mediaUnit;
romFS.size = u64(*(u32*)&header[0x1B4]) * mediaUnit; romFS.size = u64(*(u32*)&header[0x1B4]) * mediaUnit;
romFS.hashRegionSize = u64(*(u32*)&header[0x1B8]) * mediaUnit; romFS.hashRegionSize = u64(*(u32*)&header[0x1B8]) * mediaUnit;
if (fixedCryptoKey) { if (encrypted) {
Helpers::panic("Fixed crypto keys for NCCH"); Crypto::AESKey primaryKeyY;
Crypto::AESKey secondaryKeyY;
std::memcpy(primaryKeyY.data(), header, primaryKeyY.size());
if (!seedCrypto) {
secondaryKeyY = primaryKeyY;
} else {
Helpers::panic("Seed crypto is not supported");
return false;
}
auto primaryResult = getPrimaryKey(aesEngine, primaryKeyY);
if (!primaryResult.first) {
Helpers::panic("getPrimaryKey failed!");
return false;
}
Crypto::AESKey primaryKey = primaryResult.second;
auto secondaryResult = getSecondaryKey(aesEngine, secondaryKeyY);
if (!secondaryResult.first) {
Helpers::panic("getSecondaryKey failed!");
return false;
}
Crypto::AESKey secondaryKey = secondaryResult.second;
EncryptionInfo encryptionInfoTmp;
encryptionInfoTmp.normalKey = primaryKey;
encryptionInfoTmp.initialCounter.fill(0);
for (std::size_t i = 1; i <= sizeof(std::uint64_t) - 1; i++) {
encryptionInfoTmp.initialCounter[i] = header[0x108 + sizeof(std::uint64_t) - 1 - i];
}
encryptionInfoTmp.initialCounter[8] = 1;
exheaderInfo.encryptionInfo = encryptionInfoTmp;
encryptionInfoTmp.initialCounter[8] = 2;
exeFS.encryptionInfo = encryptionInfoTmp;
encryptionInfoTmp.normalKey = secondaryKey;
encryptionInfoTmp.initialCounter[8] = 3;
romFS.encryptionInfo = encryptionInfoTmp;
} }
if (exheaderSize != 0) { if (exheaderSize != 0) {
const u8* exheader = &header[0x200]; // Extended NCCH header u8 exheader[exheaderSize];
auto [success, bytes] = readFromFile(file, info, exheader, 0x200, exheaderSize);
if (!success || bytes != exheaderSize) {
printf("Failed to read Extended NCCH header\n");
return false;
}
const u64 jumpID = *(u64*)&exheader[0x1C0 + 0x8]; const u64 jumpID = *(u64*)&exheader[0x1C0 + 0x8];
// It seems like some decryption tools will decrypt the file, without actually setting the NoCrypto flag in the NCCH header // It seems like some decryption tools will decrypt the file, without actually setting the NoCrypto flag in the NCCH header
@ -46,8 +117,14 @@ bool NCCH::loadFromHeader(u8* header, IOFile& file) {
if (u32(programID) == u32(jumpID) && encrypted) { if (u32(programID) == u32(jumpID) && encrypted) {
printf("NCSD is supposedly ecrypted but not actually encrypted\n"); printf("NCSD is supposedly ecrypted but not actually encrypted\n");
encrypted = false; encrypted = false;
} else if (encrypted) { }
Helpers::panic("Encrypted NCSD file"); // If it's truely encrypted, we need to read section again.
if (encrypted) {
auto [success, bytes] = readFromFile(file, exheaderInfo, exheader, 0, exheaderSize);
if (!success || bytes != exheaderSize) {
printf("Failed to read Extended NCCH header\n");
return false;
}
} }
const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes
@ -72,8 +149,7 @@ bool NCCH::loadFromHeader(u8* header, IOFile& file) {
u8 exeFSHeader[exeFSHeaderSize]; u8 exeFSHeader[exeFSHeaderSize];
file.seek(exeFSOffset); auto [success, bytes] = readFromFile(file, exeFS, exeFSHeader, 0, exeFSHeaderSize);
auto [success, bytes] = file.readBytes(exeFSHeader, exeFSHeaderSize);
if (!success || bytes != exeFSHeaderSize) { if (!success || bytes != exeFSHeaderSize) {
printf("Failed to parse ExeFS header\n"); printf("Failed to parse ExeFS header\n");
return false; return false;
@ -105,8 +181,7 @@ bool NCCH::loadFromHeader(u8* header, IOFile& file) {
// A file offset of 0 means our file is located right after the ExeFS header // A file offset of 0 means our file is located right after the ExeFS header
// So in the ROM, files are located at (file offset + exeFS offset + exeFS header size) // So in the ROM, files are located at (file offset + exeFS offset + exeFS header size)
file.seek(exeFSOffset + exeFSHeaderSize + fileOffset); readFromFile(file, exeFS, tmp.data(), fileOffset + exeFSHeaderSize, fileSize);
file.readBytes(tmp.data(), fileSize);
// Decompress .code file from the tmp vector to the "code" vector // Decompress .code file from the tmp vector to the "code" vector
if (!CartLZ77::decompress(codeFile, tmp)) { if (!CartLZ77::decompress(codeFile, tmp)) {
@ -115,8 +190,7 @@ bool NCCH::loadFromHeader(u8* header, IOFile& file) {
} }
} else { } else {
codeFile.resize(fileSize); codeFile.resize(fileSize);
file.seek(exeFSOffset + exeFSHeaderSize + fileOffset); readFromFile(file, exeFS, codeFile.data(), fileOffset + exeFSHeaderSize, fileSize);
file.readBytes(codeFile.data(), fileSize);
} }
} }
} }
@ -130,13 +204,100 @@ bool NCCH::loadFromHeader(u8* header, IOFile& file) {
Helpers::warn("Requested stack size is %08X bytes. Temporarily emulated as 0x4000 until adjustable sizes are added\n", stackSize); Helpers::warn("Requested stack size is %08X bytes. Temporarily emulated as 0x4000 until adjustable sizes are added\n", stackSize);
} }
if (encrypted) {
if (hasExeFS())
Helpers::panic("Encrypted NCCH partition with ExeFS");
else
printf("Encrypted NCCH partition. Hopefully not required because it doesn't have an ExeFS. Skipped\n");
}
initialized = true; initialized = true;
return true; return true;
} }
std::pair<bool, Crypto::AESKey> NCCH::getPrimaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY) {
Crypto::AESKey result;
if (encrypted) {
if (fixedCryptoKey) {
return {true, result};
}
aesEngine.setKeyY(Crypto::KeySlotId::NCCHKey0, keyY);
if (!aesEngine.hasNormalKey(Crypto::KeySlotId::NCCHKey0)) {
return {false, result};
}
result = aesEngine.getNormalKey(Crypto::KeySlotId::NCCHKey0);
}
return {true, result};
}
std::pair<bool, Crypto::AESKey> NCCH::getSecondaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY) {
Crypto::AESKey result;
if (encrypted) {
if (fixedCryptoKey) {
return {true, result};
}
Crypto::KeySlotId keySlotId;
switch (secondaryKeySlot) {
case 0:
keySlotId = Crypto::KeySlotId::NCCHKey0;
break;
case 1:
keySlotId = Crypto::KeySlotId::NCCHKey1;
break;
case 10:
keySlotId = Crypto::KeySlotId::NCCHKey2;
break;
case 11:
keySlotId = Crypto::KeySlotId::NCCHKey3;
break;
default:
return {false, result};
}
if (!aesEngine.hasKeyX(keySlotId)) {
return {false, result};
}
aesEngine.setKeyY(keySlotId, keyY);
if (!aesEngine.hasNormalKey(keySlotId)) {
return {false, result};
}
result = aesEngine.getNormalKey(keySlotId);
}
return {true, result};
}
std::pair<bool, std::size_t> NCCH::readFromFile(IOFile& file, const FSInfo &info, u8 *dst, std::size_t offset, std::size_t size) {
if (size == 0) {
return { true, 0 };
}
std::size_t readMaxSize = std::min(size, static_cast<std::size_t>(info.size) - offset);
file.seek(info.offset + offset);
auto [success, bytes] = file.readBytes(dst, readMaxSize);
if (!success) {
return { success, bytes};
}
if (success && info.encryptionInfo.has_value()) {
auto& encryptionInfo = info.encryptionInfo.value();
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption d(encryptionInfo.normalKey.data(), encryptionInfo.normalKey.size(), encryptionInfo.initialCounter.data());
if (offset > 0) {
d.Seek(offset);
}
CryptoPP::byte* data = reinterpret_cast<CryptoPP::byte*>(dst);
d.ProcessData(data, data, bytes);
}
return { success, bytes};
}

View file

@ -3,7 +3,7 @@
#include "loader/ncsd.hpp" #include "loader/ncsd.hpp"
#include "memory.hpp" #include "memory.hpp"
std::optional<NCSD> Memory::loadNCSD(const std::filesystem::path& path) { std::optional<NCSD> Memory::loadNCSD(Crypto::AESEngine &aesEngine, const std::filesystem::path& path) {
NCSD ncsd; NCSD ncsd;
if (!ncsd.file.open(path, "rb")) if (!ncsd.file.open(path, "rb"))
return std::nullopt; return std::nullopt;
@ -51,18 +51,12 @@ std::optional<NCSD> Memory::loadNCSD(const std::filesystem::path& path) {
ncch.fileOffset = partition.offset; ncch.fileOffset = partition.offset;
if (partition.length != 0) { // Initialize the NCCH of each partition if (partition.length != 0) { // Initialize the NCCH of each partition
ncsd.file.seek(partition.offset); NCCH::FSInfo ncchFsInfo;
// 0x200 bytes for the NCCH header and another 0x800 for the exheader ncchFsInfo.offset = partition.offset;
constexpr u64 headerSize = 0x200 + 0x800; ncchFsInfo.size = partition.length;
u8 ncchHeader[headerSize];
std::tie(success, bytes) = ncsd.file.readBytes(ncchHeader, headerSize);
if (!success || bytes != headerSize) {
printf("Failed to read NCCH header\n");
return std::nullopt;
}
if (!ncch.loadFromHeader(ncchHeader, ncsd.file)) { if (!ncch.loadFromHeader(aesEngine, ncsd.file, ncchFsInfo)) {
printf("Invalid NCCH partition\n"); printf("Invalid NCCH partition\n");
return std::nullopt; return std::nullopt;
} }

View file

@ -161,7 +161,7 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
bool Emulator::loadNCSD(const std::filesystem::path& path) { bool Emulator::loadNCSD(const std::filesystem::path& path) {
romType = ROMType::NCSD; romType = ROMType::NCSD;
std::optional<NCSD> opt = memory.loadNCSD(path); std::optional<NCSD> opt = memory.loadNCSD(aesEngine, path);
if (!opt.has_value()) { if (!opt.has_value()) {
return false; return false;