Merge remote-tracking branch 'upstream/master' into feature/basic-controller-input

This commit is contained in:
wheremyfoodat 2023-06-28 00:20:54 +03:00
commit acd21c6908
14 changed files with 610 additions and 202 deletions

View file

@ -72,6 +72,7 @@ endif()
set(SOURCE_FILES src/main.cpp src/emulator.cpp src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp
src/core/memory.cpp
)
set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp)
set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp
src/core/kernel/memory_management.cpp src/core/kernel/ports.cpp
src/core/kernel/events.cpp src/core/kernel/threads.cpp
@ -119,6 +120,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/opengl.hpp inc
include/system_models.hpp include/services/dlp_srvr.hpp include/result/result.hpp
include/result/result_common.hpp include/result/result_fs.hpp include/result/result_fnd.hpp
include/result/result_gsp.hpp include/result/result_kernel.hpp include/result/result_os.hpp
include/crypto/aes_engine.hpp include/metaprogramming.hpp
)
set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp
@ -130,6 +132,7 @@ set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp
source_group("Header Files\\Core" FILES ${HEADER_FILES})
source_group("Source Files\\Core" FILES ${SOURCE_FILES})
source_group("Source Files\\Core\\Crypto" FILES ${CRYPTO_SOURCE_FILES})
source_group("Source Files\\Core\\Filesystem" FILES ${FS_SOURCE_FILES})
source_group("Source Files\\Core\\Kernel" FILES ${KERNEL_SOURCE_FILES})
source_group("Source Files\\Core\\Loader" FILES ${LOADER_SOURCE_FILES})
@ -138,11 +141,11 @@ source_group("Source Files\\Core\\PICA" FILES ${PICA_SOURCE_FILES})
source_group("Source Files\\Core\\OpenGL Renderer" FILES ${RENDERER_GL_SOURCE_FILES})
source_group("Source Files\\Third Party" FILES ${THIRD_PARTY_SOURCE_FILES})
add_executable(Alber ${SOURCE_FILES} ${FS_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})
target_link_libraries(Alber PRIVATE dynarmic SDL2-static glad)
target_link_libraries(Alber PRIVATE dynarmic SDL2-static glad cryptopp)
if(GPU_DEBUG_INFO)
target_compile_definitions(Alber PRIVATE GPU_DEBUG_INFO=1)
endif()
endif()

View file

@ -0,0 +1,165 @@
#pragma once
#include <array>
#include <cstring>
#include <cstdint>
#include <climits>
#include <filesystem>
#include <optional>
#include "helpers.hpp"
namespace Crypto {
constexpr std::size_t AesKeySize = 0x10;
using AESKey = std::array<u8, AesKeySize>;
template <std::size_t N>
static std::array<u8, N> rolArray(const std::array<u8, N>& value, std::size_t bits) {
const auto bitWidth = N * CHAR_BIT;
bits %= bitWidth;
const auto byteShift = bits / CHAR_BIT;
const auto bitShift = bits % CHAR_BIT;
std::array<u8, N> result;
for (std::size_t i = 0; i < N; i++) {
result[i] = ((value[(i + byteShift) % N] << bitShift) | (value[(i + byteShift + 1) % N] >> (CHAR_BIT - bitShift))) & UINT8_MAX;
}
return result;
}
template <std::size_t N>
static std::array<u8, N> addArray(const std::array<u8, N>& a, const std::array<u8, N>& b) {
std::array<u8, N> result;
std::size_t sum = 0;
std::size_t carry = 0;
for (std::int64_t i = N - 1; i >= 0; i--) {
sum = a[i] + b[i] + carry;
carry = sum >> CHAR_BIT;
result[i] = static_cast<u8>(sum & UINT8_MAX);
}
return result;
}
template <std::size_t N>
static std::array<u8, N> xorArray(const std::array<u8, N>& a, const std::array<u8, N>& b) {
std::array<u8, N> result;
for (std::size_t i = 0; i < N; i++) {
result[i] = a[i] ^ b[i];
}
return result;
}
static std::optional<AESKey> createKeyFromHex(const std::string& hex) {
if (hex.size() < 32) {
return {};
}
AESKey rawKey;
for (std::size_t i = 0; i < rawKey.size(); i++) {
rawKey[i] = static_cast<u8>(std::stoi(hex.substr(i * 2, 2), 0, 16));
}
return rawKey;
}
struct AESKeySlot {
std::optional<AESKey> keyX = std::nullopt;
std::optional<AESKey> keyY = std::nullopt;
std::optional<AESKey> normalKey = std::nullopt;
};
enum KeySlotId : std::size_t {
NCCHKey0 = 0x2C,
NCCHKey1 = 0x25,
NCCHKey2 = 0x18,
NCCHKey3 = 0x1B,
};
class AESEngine {
private:
constexpr static std::size_t AesKeySlotCount = 0x40;
std::optional<AESKey> m_generator = std::nullopt;
std::array<AESKeySlot, AesKeySlotCount> m_slots;
bool keysLoaded = false;
constexpr void updateNormalKey(std::size_t slotId) {
if (m_generator.has_value() && hasKeyX(slotId) && hasKeyY(slotId)) {
auto& keySlot = m_slots.at(slotId);
AESKey keyX = keySlot.keyX.value();
AESKey keyY = keySlot.keyY.value();
keySlot.normalKey = rolArray(addArray(xorArray(rolArray(keyX, 2), keyY), m_generator.value()), 87);
}
}
public:
AESEngine() {}
void loadKeys(const std::filesystem::path& path);
bool haveKeys() { return keysLoaded; }
constexpr bool hasKeyX(std::size_t slotId) {
if (slotId >= AesKeySlotCount) {
return false;
}
return m_slots.at(slotId).keyX.has_value();
}
constexpr AESKey getKeyX(std::size_t slotId) {
return m_slots.at(slotId).keyX.value_or(AESKey{});
}
constexpr void setKeyX(std::size_t slotId, const AESKey &key) {
if (slotId < AesKeySlotCount) {
m_slots.at(slotId).keyX = key;
updateNormalKey(slotId);
}
}
constexpr bool hasKeyY(std::size_t slotId) {
if (slotId >= AesKeySlotCount) {
return false;
}
return m_slots.at(slotId).keyY.has_value();
}
constexpr AESKey getKeyY(std::size_t slotId) {
return m_slots.at(slotId).keyY.value_or(AESKey{});
}
constexpr void setKeyY(std::size_t slotId, const AESKey &key) {
if (slotId < AesKeySlotCount) {
m_slots.at(slotId).keyY = key;
updateNormalKey(slotId);
}
}
constexpr bool hasNormalKey(std::size_t slotId) {
if (slotId >= AesKeySlotCount) {
return false;
}
return m_slots.at(slotId).normalKey.has_value();
}
constexpr AESKey getNormalKey(std::size_t slotId) {
return m_slots.at(slotId).normalKey.value_or(AESKey{});
}
constexpr void setNormalKey(std::size_t slotId, const AESKey &key) {
if (slotId < AesKeySlotCount) {
m_slots.at(slotId).normalKey = key;
}
}
};
}

View file

@ -6,6 +6,7 @@
#include <glad/gl.h>
#include "cpu.hpp"
#include "crypto/aes_engine.hpp"
#include "io_file.hpp"
#include "memory.hpp"
#include "opengl.hpp"
@ -20,6 +21,7 @@ class Emulator {
GPU gpu;
Memory memory;
Kernel kernel;
Crypto::AESEngine aesEngine;
SDL_Window* window;
SDL_GLContext glContext;

View file

@ -2,11 +2,10 @@
#include <climits>
#include <cstdarg>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <iterator>
#include <type_traits>
#include <utility>
#include <sstream>
#include <string>
#include <vector>
#include "termcolor.hpp"
@ -51,21 +50,6 @@ namespace Helpers {
va_end(args);
}
static std::vector<u8> loadROM(std::string directory) {
std::ifstream file(directory, std::ios::binary);
if (file.fail()) panic("Couldn't read %s", directory.c_str());
std::vector<u8> ROM;
file.unsetf(std::ios::skipws);
ROM.insert(ROM.begin(), std::istream_iterator<uint8_t>(file), std::istream_iterator<uint8_t>());
file.close();
printf("%s loaded successfully\n", directory.c_str());
return ROM;
}
static constexpr bool buildingInDebugMode() {
#ifdef NDEBUG
return false;
@ -120,39 +104,6 @@ namespace Helpers {
return (value >> offset) & ones<T, bits>();
}
/// Check if a bit "bit" of value is set
static constexpr bool isBitSet(u32 value, int bit) { return (value >> bit) & 1; }
/// rotate number right
template <typename T>
static constexpr T rotr(T value, int bits) {
constexpr auto bitWidth = sizeof(T) * 8;
bits &= bitWidth - 1;
return (value >> bits) | (value << (bitWidth - bits));
}
// rotate number left
template <typename T>
static constexpr T rotl(T value, int bits) {
constexpr auto bitWidth = sizeof(T) * 8;
bits &= bitWidth - 1;
return (value << bits) | (value >> (bitWidth - bits));
}
/// Used to make the compiler evaluate beeg loops at compile time for the tablegen
template <typename T, T Begin, class Func, T... Is>
static constexpr void static_for_impl(Func&& f, std::integer_sequence<T, Is...>) {
(f(std::integral_constant<T, Begin + Is>{}), ...);
}
template <typename T, T Begin, T End, class Func>
static constexpr void static_for(Func&& f) {
static_for_impl<T, Begin>(std::forward<Func>(f), std::make_integer_sequence<T, End - Begin>{});
}
// For values < 0x99
static constexpr inline u8 incBCDByte(u8 value) { return ((value & 0xf) == 0x9) ? value + 7 : value + 1; }
#ifdef HELPERS_APPLE_CLANG
template <class To, class From>
constexpr To bit_cast(const From& from) noexcept {
@ -164,6 +115,19 @@ namespace Helpers {
return std::bit_cast<To, From>(from);
}
#endif
static std::vector<std::string> split(const std::string& s, const char c) {
std::istringstream tmp(s);
std::vector<std::string> result(1);
while (std::getline(tmp, *result.rbegin(), c)) {
result.emplace_back();
}
// Remove temporary slot
result.pop_back();
return result;
}
}; // namespace Helpers
// UDLs for memory size values
@ -171,12 +135,3 @@ constexpr size_t operator""_KB(unsigned long long int x) { return 1024ULL * x; }
constexpr size_t operator""_MB(unsigned long long int x) { return 1024_KB * x; }
constexpr size_t operator""_GB(unsigned long long int x) { return 1024_MB * x; }
// useful macros
// likely/unlikely
#ifdef __GNUC__
#define likely(x) __builtin_expect((x), 1)
#define unlikely(x) __builtin_expect((x), 0)
#else
#define likely(x) (x)
#define unlikely(x) (x)
#endif

View file

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

View file

@ -0,0 +1,16 @@
#pragma once
#include <type_traits>
#include <utility>
namespace Helpers {
/// Used to make the compiler evaluate beeg loops at compile time for things like generating compile-time tables
template <typename T, T Begin, class Func, T... Is>
static constexpr void static_for_impl(Func&& f, std::integer_sequence<T, Is...>) {
(f(std::integral_constant<T, Begin + Is>{}), ...);
}
template <typename T, T Begin, T End, class Func>
static constexpr void static_for(Func&& f) {
static_for_impl<T, Begin>(std::forward<Func>(f), std::make_integer_sequence<T, End - Begin>{});
}
}

View file

@ -0,0 +1,83 @@
#include <iostream>
#include <fstream>
#include "crypto/aes_engine.hpp"
#include "helpers.hpp"
namespace Crypto {
void AESEngine::loadKeys(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::in);
if (file.fail()) {
Helpers::warn("Keys: Couldn't read key file: %s", path.c_str());
return;
}
while (!file.eof()) {
std::string line;
std::getline(file, line);
// Skip obvious invalid lines
if (line.empty() || line.starts_with("#")) {
continue;
}
const auto parts = Helpers::split(line, '=');
if (parts.size() != 2) {
Helpers::warn("Keys: Failed to parse %s", line.c_str());
continue;
}
const std::string& name = parts[0];
const std::string& rawKeyHex = parts[1];
std::size_t slotId;
char keyType;
bool is_generator = name == "generator";
if (!is_generator && std::sscanf(name.c_str(), "slot0x%zXKey%c", &slotId, &keyType) != 2) {
Helpers::warn("Keys: Ignoring unknown key %s", name.c_str());
continue;
}
auto key = createKeyFromHex(rawKeyHex);
if (!key.has_value()) {
Helpers::warn("Keys: Failed to parse raw key %s", rawKeyHex.c_str());
continue;
}
if (is_generator) {
m_generator = key;
continue;
}
if (slotId >= AesKeySlotCount) {
Helpers::warn("Keys: Invalid key slot id %u", slotId);
continue;
}
switch (keyType) {
case 'X':
setKeyX(slotId, key.value());
break;
case 'Y':
setKeyY(slotId, key.value());
break;
case 'N':
setNormalKey(slotId, key.value());
break;
default:
Helpers::warn("Keys: Invalid key type %c", keyType);
break;
}
}
// As the generator key can be set at any time, force update all normal keys.
for (std::size_t i = 0; i < AesKeySlotCount; i++) {
updateNormalKey(i);
}
keysLoaded = true;
}
};

View file

@ -133,6 +133,8 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
auto cxi = mem.getCXI();
IOFile& ioFile = mem.CXIFile;
NCCH::FSInfo fsInfo;
// Seek to file offset depending on if we're reading from RomFS, ExeFS, etc
switch (type) {
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");
}
if (!ioFile.seek(cxi->fileOffset + romFSOffset + offset + 0x1000)) {
Helpers::panic("Failed to seek while reading from RomFS");
}
fsInfo = cxi->romFS;
offset += 0x1000;
break;
}
@ -153,7 +154,7 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 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) {
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();
IOFile& ioFile = mem.CXIFile;
NCCH::FSInfo fsInfo;
// Seek to file offset depending on if we're reading from RomFS, ExeFS, etc
switch (type) {
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");
}
if (!ioFile.seek(cxi->fileOffset + romFSOffset + offset + 0x1000)) {
Helpers::panic("Failed to seek while reading from RomFS");
}
fsInfo = cxi->romFS;
offset += 0x1000;
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");
}
if (!ioFile.seek(cxi->fileOffset + exeFSOffset + offset)) { // TODO: Not sure if this needs the + 0x1000
Helpers::panic("Failed to seek while reading from ExeFS");
}
fsInfo = cxi->exeFS;
break;
}
@ -104,7 +103,7 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
}
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) {
Helpers::panic("Failed to read from SelfNCCH archive");

View file

@ -1,142 +1,311 @@
#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cstring>
#include <vector>
#include "loader/lz77.hpp"
#include "loader/ncch.hpp"
#include "memory.hpp"
bool NCCH::loadFromHeader(u8* header, IOFile& file) {
if (header[0x100] != 'N' || header[0x101] != 'C' || header[0x102] != 'C' || header[0x103] != 'H') {
printf("Invalid header on NCCH\n");
#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') {
printf("Invalid header on NCCH\n");
return false;
}
codeFile.clear();
saveData.clear();
codeFile.clear();
saveData.clear();
size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break
exheaderSize = *(u32*)&header[0x180];
size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break
exheaderSize = *(u32*)&header[0x180];
const u64 programID = *(u64*)&header[0x118];
const u64 programID = *(u64*)&header[0x118];
// Read NCCH flags
isNew3DS = header[0x188 + 4] == 2;
fixedCryptoKey = (header[0x188 + 7] & 0x1) == 0x1;
mountRomFS = (header[0x188 + 7] & 0x2) != 0x2;
encrypted = (header[0x188 + 7] & 0x4) != 0x4;
// Read ExeFS and RomFS info
exeFS.offset = u64(*(u32*)&header[0x1A0]) * mediaUnit;
exeFS.size = u64(*(u32*)&header[0x1A4]) * mediaUnit;
exeFS.hashRegionSize = u64(*(u32*)&header[0x1A8]) * mediaUnit;
// Read NCCH flags
secondaryKeySlot = header[0x188 + 3];
isNew3DS = header[0x188 + 4] == 2;
fixedCryptoKey = (header[0x188 + 7] & 0x1) == 0x1;
mountRomFS = (header[0x188 + 7] & 0x2) != 0x2;
encrypted = (header[0x188 + 7] & 0x4) != 0x4;
seedCrypto = (header[0x188 + 7] & 0x20) == 0x20;
romFS.offset = u64(*(u32*)&header[0x1B0]) * mediaUnit;
romFS.size = u64(*(u32*)&header[0x1B4]) * mediaUnit;
romFS.hashRegionSize = u64(*(u32*)&header[0x1B8]) * mediaUnit;
// Read exheader, ExeFS and RomFS info
exheaderInfo.offset = info.offset + 0x200;
exheaderInfo.size = exheaderSize;
exheaderInfo.hashRegionSize = 0;
if (fixedCryptoKey) {
Helpers::panic("Fixed crypto keys for NCCH");
}
exeFS.offset = info.offset + u64(*(u32*)&header[0x1A0]) * mediaUnit;
exeFS.size = u64(*(u32*)&header[0x1A4]) * mediaUnit;
exeFS.hashRegionSize = u64(*(u32*)&header[0x1A8]) * mediaUnit;
if (exheaderSize != 0) {
const u8* exheader = &header[0x200]; // Extended NCCH header
const u64 jumpID = *(u64*)&exheader[0x1C0 + 0x8];
romFS.offset = info.offset + u64(*(u32*)&header[0x1B0]) * mediaUnit;
romFS.size = u64(*(u32*)&header[0x1B4]) * mediaUnit;
romFS.hashRegionSize = u64(*(u32*)&header[0x1B8]) * mediaUnit;
// It seems like some decryption tools will decrypt the file, without actually setting the NoCrypto flag in the NCCH header
// This is a nice and easy hack to see if a file is pretending to be encrypted, taken from 3DMoo and Citra
if (u32(programID) == u32(jumpID) && encrypted) {
printf("NCSD is supposedly ecrypted but not actually encrypted\n");
encrypted = false;
} else if (encrypted) {
Helpers::panic("Encrypted NCSD file");
}
if (encrypted) {
if (!aesEngine.haveKeys()) {
Helpers::panic(
"Loaded an encrypted ROM but AES keys don't seem to have been provided correctly! Navigate to the emulator's\n"
"app data folder and make sure you have a sysdata directory with a file called aes_keys.txt which contains your keys!"
);
return false;
}
const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes
saveData.resize(saveDataSize, 0xff);
Crypto::AESKey primaryKeyY;
Crypto::AESKey secondaryKeyY;
std::memcpy(primaryKeyY.data(), header, primaryKeyY.size());
compressCode = (exheader[0xD] & 1) != 0;
stackSize = *(u32*)&exheader[0x1C];
bssSize = *(u32*)&exheader[0x3C];
if (!seedCrypto) {
secondaryKeyY = primaryKeyY;
} else {
Helpers::panic("Seed crypto is not supported");
return false;
}
text.extract(&exheader[0x10]);
rodata.extract(&exheader[0x20]);
data.extract(&exheader[0x30]);
}
auto primaryResult = getPrimaryKey(aesEngine, primaryKeyY);
printf("Stack size: %08X\nBSS size: %08X\n", stackSize, bssSize);
if (!primaryResult.first) {
Helpers::panic("getPrimaryKey failed!");
return false;
}
// Read ExeFS
if (hasExeFS()) {
u64 exeFSOffset = fileOffset + exeFS.offset; // Offset of ExeFS in the file = exeFS offset + ncch offset
printf("ExeFS offset: %08llX, size: %08llX (Offset in file = %08llX)\n", exeFS.offset, exeFS.size, exeFSOffset);
constexpr size_t exeFSHeaderSize = 0x200;
Crypto::AESKey primaryKey = primaryResult.second;
u8 exeFSHeader[exeFSHeaderSize];
auto secondaryResult = getSecondaryKey(aesEngine, secondaryKeyY);
file.seek(exeFSOffset);
auto [success, bytes] = file.readBytes(exeFSHeader, exeFSHeaderSize);
if (!success || bytes != exeFSHeaderSize) {
printf("Failed to parse ExeFS header\n");
return false;
}
if (!secondaryResult.first) {
Helpers::panic("getSecondaryKey failed!");
return false;
}
// ExeFS format allows up to 10 files
for (int i = 0; i < 10; i++) {
u8* fileInfo = &exeFSHeader[i * 16];
Crypto::AESKey secondaryKey = secondaryResult.second;
char name[9];
std::memcpy(name, fileInfo, 8); // Get file name as a string
name[8] = '\0'; // Add null terminator to it just in case there's none
EncryptionInfo encryptionInfoTmp;
encryptionInfoTmp.normalKey = primaryKey;
encryptionInfoTmp.initialCounter.fill(0);
u32 fileOffset = *(u32*)&fileInfo[0x8];
u32 fileSize = *(u32*)&fileInfo[0xC];
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;
if (fileSize != 0) {
printf("File %d. Name: %s, Size: %08X, Offset: %08X\n", i, name, fileSize, fileOffset);
}
encryptionInfoTmp.initialCounter[8] = 2;
exeFS.encryptionInfo = encryptionInfoTmp;
if (std::strcmp(name, ".code") == 0) {
if (hasCode()) {
Helpers::panic("Second code file in a single NCCH partition. What should this do?\n");
}
encryptionInfoTmp.normalKey = secondaryKey;
encryptionInfoTmp.initialCounter[8] = 3;
romFS.encryptionInfo = encryptionInfoTmp;
}
if (compressCode) {
std::vector<u8> tmp;
tmp.resize(fileSize);
if (exheaderSize != 0) {
std::unique_ptr<u8[]> exheader(new u8[exheaderSize]);
// 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)
file.seek(exeFSOffset + exeFSHeaderSize + fileOffset);
file.readBytes(tmp.data(), fileSize);
// Decompress .code file from the tmp vector to the "code" vector
if (!CartLZ77::decompress(codeFile, tmp)) {
printf("Failed to decompress .code file\n");
return false;
}
} else {
codeFile.resize(fileSize);
file.seek(exeFSOffset + exeFSHeaderSize + fileOffset);
file.readBytes(codeFile.data(), fileSize);
}
}
}
}
auto [success, bytes] = readFromFile(file, info, &exheader[0], 0x200, exheaderSize);
if (!success || bytes != exheaderSize) {
printf("Failed to read Extended NCCH header\n");
return false;
}
if (hasRomFS()) {
printf("RomFS offset: %08llX, size: %08llX\n", romFS.offset, romFS.size);
}
const u64 jumpID = *(u64*)&exheader[0x1C0 + 0x8];
if (stackSize != 0 && stackSize != VirtualAddrs::DefaultStackSize) {
Helpers::warn("Requested stack size is %08X bytes. Temporarily emulated as 0x4000 until adjustable sizes are added\n", stackSize);
}
// It seems like some decryption tools will decrypt the file, without actually setting the NoCrypto flag in the NCCH header
// This is a nice and easy hack to see if a file is pretending to be encrypted, taken from 3DMoo and Citra
if (u32(programID) == u32(jumpID) && encrypted) {
printf("NCSD is supposedly ecrypted but not actually encrypted\n");
encrypted = false;
}
// If it's truly encrypted, we need to read section again.
if (encrypted) {
auto [success, bytes] = readFromFile(file, exheaderInfo, &exheader[0], 0, exheaderSize);
if (!success || bytes != exheaderSize) {
printf("Failed to read Extended NCCH header\n");
return false;
}
}
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");
}
const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes
saveData.resize(saveDataSize, 0xff);
initialized = true;
return true;
compressCode = (exheader[0xD] & 1) != 0;
stackSize = *(u32*)&exheader[0x1C];
bssSize = *(u32*)&exheader[0x3C];
text.extract(&exheader[0x10]);
rodata.extract(&exheader[0x20]);
data.extract(&exheader[0x30]);
}
printf("Stack size: %08X\nBSS size: %08X\n", stackSize, bssSize);
// Read ExeFS
if (hasExeFS()) {
u64 exeFSOffset = fileOffset + exeFS.offset; // Offset of ExeFS in the file = exeFS offset + ncch offset
printf("ExeFS offset: %08llX, size: %08llX (Offset in file = %08llX)\n", exeFS.offset, exeFS.size, exeFSOffset);
constexpr size_t exeFSHeaderSize = 0x200;
u8 exeFSHeader[exeFSHeaderSize];
auto [success, bytes] = readFromFile(file, exeFS, exeFSHeader, 0, exeFSHeaderSize);
if (!success || bytes != exeFSHeaderSize) {
printf("Failed to parse ExeFS header\n");
return false;
}
// ExeFS format allows up to 10 files
for (int i = 0; i < 10; i++) {
u8* fileInfo = &exeFSHeader[i * 16];
char name[9];
std::memcpy(name, fileInfo, 8); // Get file name as a string
name[8] = '\0'; // Add null terminator to it just in case there's none
u32 fileOffset = *(u32*)&fileInfo[0x8];
u32 fileSize = *(u32*)&fileInfo[0xC];
if (fileSize != 0) {
printf("File %d. Name: %s, Size: %08X, Offset: %08X\n", i, name, fileSize, fileOffset);
}
if (std::strcmp(name, ".code") == 0) {
if (hasCode()) {
Helpers::panic("Second code file in a single NCCH partition. What should this do?\n");
}
if (compressCode) {
std::vector<u8> tmp;
tmp.resize(fileSize);
// 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)
readFromFile(file, exeFS, tmp.data(), fileOffset + exeFSHeaderSize, fileSize);
// Decompress .code file from the tmp vector to the "code" vector
if (!CartLZ77::decompress(codeFile, tmp)) {
printf("Failed to decompress .code file\n");
return false;
}
} else {
codeFile.resize(fileSize);
readFromFile(file, exeFS, codeFile.data(), fileOffset + exeFSHeaderSize, fileSize);
}
}
}
}
if (hasRomFS()) {
printf("RomFS offset: %08llX, size: %08llX\n", romFS.offset, romFS.size);
}
if (stackSize != 0 && stackSize != VirtualAddrs::DefaultStackSize) {
Helpers::warn("Requested stack size is %08X bytes. Temporarily emulated as 0x4000 until adjustable sizes are added\n", stackSize);
}
initialized = 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 "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;
if (!ncsd.file.open(path, "rb"))
return std::nullopt;
@ -51,18 +51,12 @@ std::optional<NCSD> Memory::loadNCSD(const std::filesystem::path& path) {
ncch.fileOffset = partition.offset;
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
constexpr u64 headerSize = 0x200 + 0x800;
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;
}
ncchFsInfo.offset = partition.offset;
ncchFsInfo.size = partition.length;
if (!ncch.loadFromHeader(ncchHeader, ncsd.file)) {
if (!ncch.loadFromHeader(aesEngine, ncsd.file, ncchFsInfo)) {
printf("Invalid NCCH partition\n");
return std::nullopt;
}

View file

@ -203,9 +203,18 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
// Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in
// %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart
char* appData = SDL_GetPrefPath(nullptr, "Alber");
const std::filesystem::path dataPath = std::filesystem::path(appData) / path.filename().stem();
const std::filesystem::path appDataPath = std::filesystem::path(appData);
const std::filesystem::path dataPath = appDataPath / path.filename().stem();
const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt";
IOFile::setAppDataDir(dataPath);
SDL_free(appData);
SDL_free(appData);
// Open the text file containing our AES keys if it exists. We use the std::filesystem::exists overload that takes an error code param to
// avoid the call throwing exceptions
std::error_code ec;
if (std::filesystem::exists(aesKeysPath, ec) && !ec) {
aesEngine.loadKeys(aesKeysPath);
}
kernel.initializeFS();
auto extension = path.extension();
@ -222,7 +231,7 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
bool Emulator::loadNCSD(const std::filesystem::path& path) {
romType = ROMType::NCSD;
std::optional<NCSD> opt = memory.loadNCSD(path);
std::optional<NCSD> opt = memory.loadNCSD(aesEngine, path);
if (!opt.has_value()) {
return false;

View file

@ -5,7 +5,7 @@ int main (int argc, char *argv[]) {
emu.initGraphicsContext();
auto romPath = std::filesystem::current_path() / (argc > 1 ? argv[1] : "Metroid Prime - Federation Force (Europe) (En,Fr,De,Es,It).3ds");
auto romPath = std::filesystem::current_path() / (argc > 1 ? argv[1] : "OoT Demo Encrypted.3ds");
if (!emu.loadROM(romPath)) {
// For some reason just .c_str() doesn't show the proper path
Helpers::panic("Failed to load ROM file: %s", romPath.string().c_str());