From f572373fc13468354c4d418faa759fdf711858dd Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:29:18 +0300 Subject: [PATCH] AES: Implement seed crypto --- include/crypto/aes_engine.hpp | 79 +++++++++++++++++------------- src/core/crypto/aes_engine.cpp | 89 +++++++++++++++++++++++++++------- src/core/loader/ncch.cpp | 34 ++++++++++--- src/emulator.cpp | 6 +++ 4 files changed, 151 insertions(+), 57 deletions(-) diff --git a/include/crypto/aes_engine.hpp b/include/crypto/aes_engine.hpp index 324f4adf..f8a2d7e4 100644 --- a/include/crypto/aes_engine.hpp +++ b/include/crypto/aes_engine.hpp @@ -1,20 +1,29 @@ #pragma once #include -#include -#include #include +#include +#include #include #include +#include #include "helpers.hpp" +#include "io_file.hpp" +#include "swap.hpp" namespace Crypto { - constexpr std::size_t AesKeySize = 0x10; + constexpr usize AesKeySize = 0x10; using AESKey = std::array; - template - static std::array rolArray(const std::array& value, std::size_t bits) { + struct Seed { + u64_le titleID; + AESKey seed; + std::array pad; + }; + + template + static std::array rolArray(const std::array& value, usize bits) { const auto bitWidth = N * CHAR_BIT; bits %= bitWidth; @@ -24,18 +33,18 @@ namespace Crypto { std::array result; - for (std::size_t i = 0; i < N; i++) { + for (usize 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 + template static std::array addArray(const std::array& a, const std::array& b) { std::array result; - std::size_t sum = 0; - std::size_t carry = 0; + usize sum = 0; + usize carry = 0; for (std::int64_t i = N - 1; i >= 0; i--) { sum = a[i] + b[i] + carry; @@ -46,11 +55,11 @@ namespace Crypto { return result; } - template + template static std::array xorArray(const std::array& a, const std::array& b) { std::array result; - for (std::size_t i = 0; i < N; i++) { + for (usize i = 0; i < N; i++) { result[i] = a[i] ^ b[i]; } @@ -63,7 +72,7 @@ namespace Crypto { } AESKey rawKey; - for (std::size_t i = 0; i < rawKey.size(); i++) { + for (usize i = 0; i < rawKey.size(); i++) { rawKey[i] = static_cast(std::stoi(hex.substr(i * 2, 2), 0, 16)); } @@ -76,7 +85,7 @@ namespace Crypto { std::optional normalKey = std::nullopt; }; - enum KeySlotId : std::size_t { + enum KeySlotId : usize { NCCHKey0 = 0x2C, NCCHKey1 = 0x25, NCCHKey2 = 0x18, @@ -84,14 +93,18 @@ namespace Crypto { }; class AESEngine { - private: - constexpr static std::size_t AesKeySlotCount = 0x40; + private: + constexpr static usize AesKeySlotCount = 0x40; std::optional m_generator = std::nullopt; std::array m_slots; bool keysLoaded = false; - constexpr void updateNormalKey(std::size_t slotId) { + std::vector seeds; + IOFile seedDatabase; + bool seedsLoaded = false; + + constexpr void updateNormalKey(usize slotId) { if (m_generator.has_value() && hasKeyX(slotId) && hasKeyY(slotId)) { auto& keySlot = m_slots.at(slotId); AESKey keyX = keySlot.keyX.value(); @@ -101,13 +114,17 @@ namespace Crypto { } } - public: + public: AESEngine() {} void loadKeys(const std::filesystem::path& path); + void setSeedPath(const std::filesystem::path& path); + // Returns true on success, false on failure + bool loadSeeds(); + bool haveKeys() { return keysLoaded; } bool haveGenerator() { return m_generator.has_value(); } - constexpr bool hasKeyX(std::size_t slotId) { + constexpr bool hasKeyX(usize slotId) { if (slotId >= AesKeySlotCount) { return false; } @@ -115,18 +132,16 @@ namespace Crypto { 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 AESKey getKeyX(usize slotId) { return m_slots.at(slotId).keyX.value_or(AESKey{}); } - constexpr void setKeyX(std::size_t slotId, const AESKey &key) { + constexpr void setKeyX(usize slotId, const AESKey& key) { if (slotId < AesKeySlotCount) { m_slots.at(slotId).keyX = key; updateNormalKey(slotId); } } - constexpr bool hasKeyY(std::size_t slotId) { + constexpr bool hasKeyY(usize slotId) { if (slotId >= AesKeySlotCount) { return false; } @@ -134,18 +149,16 @@ namespace Crypto { 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 AESKey getKeyY(usize slotId) { return m_slots.at(slotId).keyY.value_or(AESKey{}); } - constexpr void setKeyY(std::size_t slotId, const AESKey &key) { + constexpr void setKeyY(usize slotId, const AESKey& key) { if (slotId < AesKeySlotCount) { m_slots.at(slotId).keyY = key; updateNormalKey(slotId); } } - constexpr bool hasNormalKey(std::size_t slotId) { + constexpr bool hasNormalKey(usize slotId) { if (slotId >= AesKeySlotCount) { return false; } @@ -153,14 +166,14 @@ namespace Crypto { 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 AESKey getNormalKey(usize slotId) { return m_slots.at(slotId).normalKey.value_or(AESKey{}); } - constexpr void setNormalKey(std::size_t slotId, const AESKey &key) { + constexpr void setNormalKey(usize slotId, const AESKey& key) { if (slotId < AesKeySlotCount) { m_slots.at(slotId).normalKey = key; } } + + std::optional getSeedFromDB(u64 titleID); }; -} \ No newline at end of file +} // namespace Crypto \ No newline at end of file diff --git a/src/core/crypto/aes_engine.cpp b/src/core/crypto/aes_engine.cpp index f4bf3494..dc3ae060 100644 --- a/src/core/crypto/aes_engine.cpp +++ b/src/core/crypto/aes_engine.cpp @@ -1,13 +1,15 @@ -#include -#include - #include "crypto/aes_engine.hpp" + +#include +#include +#include + #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; @@ -58,18 +60,10 @@ namespace Crypto { } 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; + 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; } } @@ -80,4 +74,65 @@ namespace Crypto { keysLoaded = true; } -}; \ No newline at end of file + + void AESEngine::setSeedPath(const std::filesystem::path& path) { seedDatabase.open(path, "rb"); } + + // Loads seeds from a seed file, return true on success and false on failure + bool AESEngine::loadSeeds() { + if (!seedDatabase.isOpen()) { + return false; + } + + // The # of seeds is stored at offset 0 + u32_le seedCount = 0; + + if (!seedDatabase.rewind()) { + return false; + } + + auto [success, size] = seedDatabase.readBytes(&seedCount, sizeof(u32)); + if (!success || size != sizeof(u32)) { + return false; + } + + // Key data starts from offset 16 + if (!seedDatabase.seek(16)) { + return false; + } + + Crypto::Seed seed; + for (uint i = 0; i < seedCount; i++) { + std::tie(success, size) = seedDatabase.readBytes(&seed, sizeof(seed)); + if (!success || size != sizeof(seed)) { + return false; + } + + seeds.push_back(seed); + } + + return true; + } + + std::optional AESEngine::getSeedFromDB(u64 titleID) { + // We don't have a seed db nor any seeds loaded, return nullopt + if (!seedDatabase.isOpen() && seeds.empty()) { + return std::nullopt; + } + + // We have a seed DB but haven't loaded the seeds yet, so load them + if (seedDatabase.isOpen() && seeds.empty()) { + bool success = loadSeeds(); + if (!success) { + return std::nullopt; + } + } + + for (Crypto::Seed& seed : seeds) { + if (seed.titleID == titleID) { + return seed.seed; + } + } + + return std::nullopt; + } +}; // namespace Crypto \ No newline at end of file diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index a575d4f2..3a7cb1f6 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -1,12 +1,15 @@ +#include "loader/ncch.hpp" + #include #include -#include -#include -#include "loader/lz77.hpp" -#include "loader/ncch.hpp" -#include "memory.hpp" +#include +#include #include +#include + +#include "loader/lz77.hpp" +#include "memory.hpp" bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info) { // 0x200 bytes for the NCCH header @@ -70,8 +73,25 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn if (!seedCrypto) { secondaryKeyY = primaryKeyY; } else { - Helpers::warn("Seed crypto is not supported"); - gotCryptoKeys = false; + // In seed crypto mode, the secondary key is computed through a SHA256 hash of the primary key and a title-specific seed, which we fetch + // from seeddb.bin + std::optional seedOptional = aesEngine.getSeedFromDB(programID); + if (seedOptional.has_value()) { + auto seed = *seedOptional; + + CryptoPP::SHA256 shaEngine; + std::array data; + std::array hash; + + std::memcpy(&data[0], primaryKeyY.data(), primaryKeyY.size()); + std::memcpy(&data[16], seed.data(), seed.size()); + shaEngine.CalculateDigest(hash.data(), data.data(), data.size()); + // Note that SHA256 will produce a 256-bit hash, while we only need 128 bits cause this is an AES key + // So the latter 16 bytes of the SHA256 are thrown out. + std::memcpy(secondaryKeyY.data(), hash.data(), secondaryKeyY.size()); + } else { + Helpers::warn("Couldn't find a seed value for this title. Make sure you have a seeddb.bin file alongside your aes_keys.txt"); + } } auto primaryResult = getPrimaryKey(aesEngine, primaryKeyY); diff --git a/src/emulator.cpp b/src/emulator.cpp index 921af08f..e4bfc4af 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -220,6 +220,8 @@ bool Emulator::loadROM(const std::filesystem::path& path) { const std::filesystem::path appDataPath = getAppDataRoot(); const std::filesystem::path dataPath = appDataPath / path.filename().stem(); const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt"; + const std::filesystem::path seedDBPath = appDataPath / "sysdata" / "seeddb.bin"; + IOFile::setAppDataDir(dataPath); // 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 @@ -229,6 +231,10 @@ bool Emulator::loadROM(const std::filesystem::path& path) { aesEngine.loadKeys(aesKeysPath); } + if (std::filesystem::exists(seedDBPath, ec) && !ec) { + aesEngine.setSeedPath(seedDBPath); + } + kernel.initializeFS(); auto extension = path.extension(); bool success; // Tracks if we loaded the ROM successfully