mirror of
https://github.com/wheremyfoodat/Panda3DS.git
synced 2025-04-07 14:45:41 +12:00
Merge pull request #567 from wheremyfoodat/seed-crypto
Implement seed crypto
This commit is contained in:
commit
91ebff1b5f
4 changed files with 151 additions and 57 deletions
|
@ -1,20 +1,29 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstring>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <climits>
|
#include <climits>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "helpers.hpp"
|
#include "helpers.hpp"
|
||||||
|
#include "io_file.hpp"
|
||||||
|
#include "swap.hpp"
|
||||||
|
|
||||||
namespace Crypto {
|
namespace Crypto {
|
||||||
constexpr std::size_t AesKeySize = 0x10;
|
constexpr usize AesKeySize = 0x10;
|
||||||
using AESKey = std::array<u8, AesKeySize>;
|
using AESKey = std::array<u8, AesKeySize>;
|
||||||
|
|
||||||
template <std::size_t N>
|
struct Seed {
|
||||||
static std::array<u8, N> rolArray(const std::array<u8, N>& value, std::size_t bits) {
|
u64_le titleID;
|
||||||
|
AESKey seed;
|
||||||
|
std::array<u8, 8> pad;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <usize N>
|
||||||
|
static std::array<u8, N> rolArray(const std::array<u8, N>& value, usize bits) {
|
||||||
const auto bitWidth = N * CHAR_BIT;
|
const auto bitWidth = N * CHAR_BIT;
|
||||||
|
|
||||||
bits %= bitWidth;
|
bits %= bitWidth;
|
||||||
|
@ -24,18 +33,18 @@ namespace Crypto {
|
||||||
|
|
||||||
std::array<u8, N> result;
|
std::array<u8, N> 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;
|
result[i] = ((value[(i + byteShift) % N] << bitShift) | (value[(i + byteShift + 1) % N] >> (CHAR_BIT - bitShift))) & UINT8_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <std::size_t N>
|
template <usize N>
|
||||||
static std::array<u8, N> addArray(const std::array<u8, N>& a, const std::array<u8, N>& b) {
|
static std::array<u8, N> addArray(const std::array<u8, N>& a, const std::array<u8, N>& b) {
|
||||||
std::array<u8, N> result;
|
std::array<u8, N> result;
|
||||||
std::size_t sum = 0;
|
usize sum = 0;
|
||||||
std::size_t carry = 0;
|
usize carry = 0;
|
||||||
|
|
||||||
for (std::int64_t i = N - 1; i >= 0; i--) {
|
for (std::int64_t i = N - 1; i >= 0; i--) {
|
||||||
sum = a[i] + b[i] + carry;
|
sum = a[i] + b[i] + carry;
|
||||||
|
@ -46,11 +55,11 @@ namespace Crypto {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <std::size_t N>
|
template <usize N>
|
||||||
static std::array<u8, N> xorArray(const std::array<u8, N>& a, const std::array<u8, N>& b) {
|
static std::array<u8, N> xorArray(const std::array<u8, N>& a, const std::array<u8, N>& b) {
|
||||||
std::array<u8, N> result;
|
std::array<u8, N> result;
|
||||||
|
|
||||||
for (std::size_t i = 0; i < N; i++) {
|
for (usize i = 0; i < N; i++) {
|
||||||
result[i] = a[i] ^ b[i];
|
result[i] = a[i] ^ b[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +72,7 @@ namespace Crypto {
|
||||||
}
|
}
|
||||||
|
|
||||||
AESKey rawKey;
|
AESKey rawKey;
|
||||||
for (std::size_t i = 0; i < rawKey.size(); i++) {
|
for (usize i = 0; i < rawKey.size(); i++) {
|
||||||
rawKey[i] = static_cast<u8>(std::stoi(hex.substr(i * 2, 2), 0, 16));
|
rawKey[i] = static_cast<u8>(std::stoi(hex.substr(i * 2, 2), 0, 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +85,7 @@ namespace Crypto {
|
||||||
std::optional<AESKey> normalKey = std::nullopt;
|
std::optional<AESKey> normalKey = std::nullopt;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum KeySlotId : std::size_t {
|
enum KeySlotId : usize {
|
||||||
NCCHKey0 = 0x2C,
|
NCCHKey0 = 0x2C,
|
||||||
NCCHKey1 = 0x25,
|
NCCHKey1 = 0x25,
|
||||||
NCCHKey2 = 0x18,
|
NCCHKey2 = 0x18,
|
||||||
|
@ -84,14 +93,17 @@ namespace Crypto {
|
||||||
};
|
};
|
||||||
|
|
||||||
class AESEngine {
|
class AESEngine {
|
||||||
private:
|
private:
|
||||||
constexpr static std::size_t AesKeySlotCount = 0x40;
|
constexpr static usize AesKeySlotCount = 0x40;
|
||||||
|
|
||||||
std::optional<AESKey> m_generator = std::nullopt;
|
std::optional<AESKey> m_generator = std::nullopt;
|
||||||
std::array<AESKeySlot, AesKeySlotCount> m_slots;
|
std::array<AESKeySlot, AesKeySlotCount> m_slots;
|
||||||
bool keysLoaded = false;
|
bool keysLoaded = false;
|
||||||
|
|
||||||
constexpr void updateNormalKey(std::size_t slotId) {
|
std::vector<Seed> seeds;
|
||||||
|
IOFile seedDatabase;
|
||||||
|
|
||||||
|
constexpr void updateNormalKey(usize slotId) {
|
||||||
if (m_generator.has_value() && hasKeyX(slotId) && hasKeyY(slotId)) {
|
if (m_generator.has_value() && hasKeyX(slotId) && hasKeyY(slotId)) {
|
||||||
auto& keySlot = m_slots.at(slotId);
|
auto& keySlot = m_slots.at(slotId);
|
||||||
AESKey keyX = keySlot.keyX.value();
|
AESKey keyX = keySlot.keyX.value();
|
||||||
|
@ -101,13 +113,17 @@ namespace Crypto {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AESEngine() {}
|
AESEngine() {}
|
||||||
void loadKeys(const std::filesystem::path& path);
|
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 haveKeys() { return keysLoaded; }
|
||||||
bool haveGenerator() { return m_generator.has_value(); }
|
bool haveGenerator() { return m_generator.has_value(); }
|
||||||
|
|
||||||
constexpr bool hasKeyX(std::size_t slotId) {
|
constexpr bool hasKeyX(usize slotId) {
|
||||||
if (slotId >= AesKeySlotCount) {
|
if (slotId >= AesKeySlotCount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -115,18 +131,16 @@ namespace Crypto {
|
||||||
return m_slots.at(slotId).keyX.has_value();
|
return m_slots.at(slotId).keyX.has_value();
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr AESKey getKeyX(std::size_t slotId) {
|
constexpr AESKey getKeyX(usize slotId) { return m_slots.at(slotId).keyX.value_or(AESKey{}); }
|
||||||
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) {
|
if (slotId < AesKeySlotCount) {
|
||||||
m_slots.at(slotId).keyX = key;
|
m_slots.at(slotId).keyX = key;
|
||||||
updateNormalKey(slotId);
|
updateNormalKey(slotId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr bool hasKeyY(std::size_t slotId) {
|
constexpr bool hasKeyY(usize slotId) {
|
||||||
if (slotId >= AesKeySlotCount) {
|
if (slotId >= AesKeySlotCount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -134,18 +148,16 @@ namespace Crypto {
|
||||||
return m_slots.at(slotId).keyY.has_value();
|
return m_slots.at(slotId).keyY.has_value();
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr AESKey getKeyY(std::size_t slotId) {
|
constexpr AESKey getKeyY(usize slotId) { return m_slots.at(slotId).keyY.value_or(AESKey{}); }
|
||||||
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) {
|
if (slotId < AesKeySlotCount) {
|
||||||
m_slots.at(slotId).keyY = key;
|
m_slots.at(slotId).keyY = key;
|
||||||
updateNormalKey(slotId);
|
updateNormalKey(slotId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr bool hasNormalKey(std::size_t slotId) {
|
constexpr bool hasNormalKey(usize slotId) {
|
||||||
if (slotId >= AesKeySlotCount) {
|
if (slotId >= AesKeySlotCount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -153,14 +165,14 @@ namespace Crypto {
|
||||||
return m_slots.at(slotId).normalKey.has_value();
|
return m_slots.at(slotId).normalKey.has_value();
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr AESKey getNormalKey(std::size_t slotId) {
|
constexpr AESKey getNormalKey(usize slotId) { return m_slots.at(slotId).normalKey.value_or(AESKey{}); }
|
||||||
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) {
|
if (slotId < AesKeySlotCount) {
|
||||||
m_slots.at(slotId).normalKey = key;
|
m_slots.at(slotId).normalKey = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<AESKey> getSeedFromDB(u64 titleID);
|
||||||
};
|
};
|
||||||
}
|
} // namespace Crypto
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
#include <iostream>
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
#include "crypto/aes_engine.hpp"
|
#include "crypto/aes_engine.hpp"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
#include "helpers.hpp"
|
#include "helpers.hpp"
|
||||||
|
|
||||||
namespace Crypto {
|
namespace Crypto {
|
||||||
void AESEngine::loadKeys(const std::filesystem::path& path) {
|
void AESEngine::loadKeys(const std::filesystem::path& path) {
|
||||||
std::ifstream file(path, std::ios::in);
|
std::ifstream file(path, std::ios::in);
|
||||||
|
|
||||||
if (file.fail()) {
|
if (file.fail()) {
|
||||||
Helpers::warn("Keys: Couldn't read key file: %s", path.c_str());
|
Helpers::warn("Keys: Couldn't read key file: %s", path.c_str());
|
||||||
return;
|
return;
|
||||||
|
@ -58,18 +60,10 @@ namespace Crypto {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keyType) {
|
switch (keyType) {
|
||||||
case 'X':
|
case 'X': setKeyX(slotId, key.value()); break;
|
||||||
setKeyX(slotId, key.value());
|
case 'Y': setKeyY(slotId, key.value()); break;
|
||||||
break;
|
case 'N': setNormalKey(slotId, key.value()); break;
|
||||||
case 'Y':
|
default: Helpers::warn("Keys: Invalid key type %c", keyType); break;
|
||||||
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;
|
keysLoaded = true;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
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<Crypto::AESKey> 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
|
|
@ -1,12 +1,15 @@
|
||||||
|
#include "loader/ncch.hpp"
|
||||||
|
|
||||||
#include <cryptopp/aes.h>
|
#include <cryptopp/aes.h>
|
||||||
#include <cryptopp/modes.h>
|
#include <cryptopp/modes.h>
|
||||||
#include <cstring>
|
#include <cryptopp/sha.h>
|
||||||
#include <vector>
|
|
||||||
#include "loader/lz77.hpp"
|
|
||||||
#include "loader/ncch.hpp"
|
|
||||||
#include "memory.hpp"
|
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "loader/lz77.hpp"
|
||||||
|
#include "memory.hpp"
|
||||||
|
|
||||||
bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info) {
|
bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info) {
|
||||||
// 0x200 bytes for the NCCH header
|
// 0x200 bytes for the NCCH header
|
||||||
|
@ -70,8 +73,26 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
|
||||||
if (!seedCrypto) {
|
if (!seedCrypto) {
|
||||||
secondaryKeyY = primaryKeyY;
|
secondaryKeyY = primaryKeyY;
|
||||||
} else {
|
} else {
|
||||||
Helpers::warn("Seed crypto is not supported");
|
// 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
|
||||||
gotCryptoKeys = false;
|
// from seeddb.bin
|
||||||
|
std::optional<Crypto::AESKey> seedOptional = aesEngine.getSeedFromDB(programID);
|
||||||
|
if (seedOptional.has_value()) {
|
||||||
|
auto seed = *seedOptional;
|
||||||
|
|
||||||
|
CryptoPP::SHA256 shaEngine;
|
||||||
|
std::array<u8, 32> data;
|
||||||
|
std::array<u8, CryptoPP::SHA256::DIGESTSIZE> 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");
|
||||||
|
gotCryptoKeys = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto primaryResult = getPrimaryKey(aesEngine, primaryKeyY);
|
auto primaryResult = getPrimaryKey(aesEngine, primaryKeyY);
|
||||||
|
|
|
@ -220,6 +220,8 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
|
||||||
const std::filesystem::path appDataPath = getAppDataRoot();
|
const std::filesystem::path appDataPath = getAppDataRoot();
|
||||||
const std::filesystem::path dataPath = appDataPath / path.filename().stem();
|
const std::filesystem::path dataPath = appDataPath / path.filename().stem();
|
||||||
const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt";
|
const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt";
|
||||||
|
const std::filesystem::path seedDBPath = appDataPath / "sysdata" / "seeddb.bin";
|
||||||
|
|
||||||
IOFile::setAppDataDir(dataPath);
|
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
|
// 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);
|
aesEngine.loadKeys(aesKeysPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std::filesystem::exists(seedDBPath, ec) && !ec) {
|
||||||
|
aesEngine.setSeedPath(seedDBPath);
|
||||||
|
}
|
||||||
|
|
||||||
kernel.initializeFS();
|
kernel.initializeFS();
|
||||||
auto extension = path.extension();
|
auto extension = path.extension();
|
||||||
bool success; // Tracks if we loaded the ROM successfully
|
bool success; // Tracks if we loaded the ROM successfully
|
||||||
|
|
Loading…
Add table
Reference in a new issue