1 Commits

Author SHA1 Message Date
patchzyy 1ab77952f4 Load console identity from NAND setting.txt 2026-09-05 22:36:56 +02:00
5 changed files with 188 additions and 104 deletions
+5
View File
@@ -277,6 +277,11 @@ target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17) target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests) add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
# The input expression engine is self-contained, so it can be exercised without # The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL. # linking the runtime or SDL.
add_executable(mkw_input_expr_tests add_executable(mkw_input_expr_tests
+20 -89
View File
@@ -1,38 +1,28 @@
#pragma once #pragma once
#include "runtime_config.h" #include "nand_path.h"
#include "nand_settings.h"
#include <algorithm>
#include <array> #include <array>
#include <cctype>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip> #include <iomanip>
#include <optional>
#include <random>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view>
#include <utility> #include <utility>
namespace RuntimeConsoleIdentity { namespace RuntimeConsoleIdentity {
struct Identity { struct Identity {
std::string serial; std::string serial;
std::string productCode;
std::string area;
std::string gameRegion;
std::array<uint8_t, 6> mac; std::array<uint8_t, 6> mac;
}; };
inline bool IsValidSerial(const std::string& serial) {
return serial.size() == 9 &&
serial != "000000000" &&
std::all_of(serial.begin(), serial.end(),
[](unsigned char value) { return std::isdigit(value) != 0; });
}
inline Identity FromSerial(std::string serial) { inline Identity FromSerial(std::string serial) {
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial // Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
// so every API exposes one coherent, stable virtual-console identity. // so every API exposes one coherent, stable virtual-console identity.
uint32_t hash = 2166136261u; uint32_t hash = 2166136261u;
for (const unsigned char value : serial) { for (const unsigned char value : serial) {
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
return { return {
std::move(serial), std::move(serial),
{}, {}, {},
{ {
0x00, 0x00,
0x09, 0x09,
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
}; };
} }
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) { inline Identity LoadFromNand() {
std::ifstream input(path); const auto root = RuntimeNandPath::DiscoverNandRootPath();
std::string line; const auto settings = RuntimeNandSettings::Read(root);
if (!input || !std::getline(input, line)) { if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
return std::nullopt; RuntimeNandPath::FailNandRoot(
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
root / "title/00000001/00000002/data/setting.txt");
} }
constexpr std::string_view prefix = "serial="; Identity identity = FromSerial(settings->at("SERNO"));
if (line.rfind(prefix, 0) != 0) { identity.productCode = settings->at("CODE");
return std::nullopt; identity.area = settings->at("AREA");
} identity.gameRegion = settings->at("GAME");
std::string serial = line.substr(prefix.size()); return identity;
if (!IsValidSerial(serial)) {
return std::nullopt;
}
return serial;
}
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
return false;
}
std::filesystem::path temporary = path;
temporary += ".tmp";
{
std::ofstream output(temporary, std::ios::trunc);
if (!output) {
return false;
}
output << "serial=" << serial << '\n';
output.close();
if (!output) {
return false;
}
}
std::filesystem::rename(temporary, path, ec);
if (!ec) {
return true;
}
std::filesystem::remove(temporary, ec);
return false;
}
inline std::string GenerateSerial() {
std::random_device entropy;
std::seed_seq seed{
entropy(),
entropy(),
entropy(),
entropy(),
};
std::mt19937 generator(seed);
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
return std::to_string(distribution(generator));
}
inline Identity LoadOrCreate(const std::filesystem::path& path) {
if (const auto serial = ReadSerial(path)) {
return FromSerial(*serial);
}
const std::string generated = GenerateSerial();
if (WriteSerial(path, generated)) {
return FromSerial(generated);
}
// Remain operational in a read-only environment. This fallback matches
// Dolphin's deterministic serial while keeping the same valid identity shape.
return FromSerial("123456789");
} }
inline const Identity& Current() { inline const Identity& Current() {
static const Identity identity = static const Identity identity = LoadFromNand();
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
return identity; return identity;
} }
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <array>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <map>
#include <optional>
#include <string>
#include <utility>
namespace RuntimeNandSettings {
using Settings = std::map<std::string, std::string>;
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
std::ifstream input(nandRoot / "title/00000001/00000002/data/setting.txt",
std::ios::binary);
std::array<uint8_t, 256> bytes{};
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
return std::nullopt;
}
uint32_t key = 0x73B5DBFAu;
std::string decoded;
for (const uint8_t byte : bytes) {
const char value = static_cast<char>(byte ^ static_cast<uint8_t>(key));
key = (key << 1) | (key >> 31);
if (value == '\0') {
break;
}
if (value != '\r') {
decoded += value;
}
}
Settings settings;
for (size_t start = 0; start < decoded.size();) {
const size_t end = decoded.find('\n', start);
const std::string line = decoded.substr(start, end - start);
const size_t equals = line.find('=');
if (equals != std::string::npos && equals != 0) {
settings.emplace(line.substr(0, equals), line.substr(equals + 1));
}
if (end == std::string::npos) {
break;
}
start = end + 1;
}
return settings;
}
inline bool HasIdentity(const Settings& settings) {
const auto serial = settings.find("SERNO");
if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 ||
serial->second.find_first_not_of("0123456789") != std::string::npos ||
serial->second.find_first_not_of('0') == std::string::npos) {
return false;
}
for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) {
const auto value = settings.find(field.first);
if (value == settings.end() || value->second.empty() ||
value->second.size() > field.second) {
return false;
}
}
return true;
}
} // namespace RuntimeNandSettings
+29 -15
View File
@@ -12,7 +12,25 @@
namespace { namespace {
constexpr uint32_t kPalProductRegion = 2; // Use the SDK's own value tables, including its unknown-region result.
uint32_t LookupProductRegion(uint32_t table, uint32_t stride, uint32_t count,
const std::string& value) {
for (uint32_t index = 0; index < count; ++index) {
const uint32_t entry = table + index * stride;
if (!Memory::Contains(entry, stride)) {
break;
}
const auto* bytes = static_cast<const uint8_t*>(Memory::GetPointer(entry, stride));
if (bytes[0] == 0xFF) {
break;
}
if (value.size() < stride - 1 &&
std::memcmp(bytes + 1, value.c_str(), value.size() + 1) == 0) {
return bytes[0];
}
}
return 0xFFFFFFFFu;
}
} // namespace } // namespace
@@ -50,16 +68,12 @@ extern "C" uint32_t SCGetEuRgb60Mode_HLE()
PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ());
// The managed NAND intentionally starts without a console-owned setting.txt. // Expose the selected emulated NAND identity through the SDK SC APIs.
// DWC nevertheless requires the Wii product code and serial number so it can
// include csnum in NAS authentication. Expose one stable virtual-console
// identity without requiring or mutating a user's real NAND.
extern "C" uint32_t SCGetProductArea_HLE() extern "C" uint32_t SCGetProductArea_HLE()
{ {
// The PAL setting.txt AREA value is "EUR". The SDK's lookup table at return LookupProductRegion(0x8029CEB0u, 5, 13,
// 0x8029CEB0 maps JPN=0, USA=1, EUR=2. RuntimeConsoleIdentity::Current().area);
return kPalProductRegion;
} }
PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ());
@@ -68,12 +82,13 @@ extern "C" uint32_t SCGetProductCode_HLE()
{ {
// Original PAL SC storage for the six-byte CODE value. // Original PAL SC storage for the six-byte CODE value.
constexpr uint32_t kProductCodeAddress = 0x803869E0u; constexpr uint32_t kProductCodeAddress = 0x803869E0u;
static constexpr char kProductCode[] = "LEH"; const std::string& productCode = RuntimeConsoleIdentity::Current().productCode;
if (!Memory::Contains(kProductCodeAddress, sizeof(kProductCode))) { const size_t size = productCode.size() + 1;
if (!Memory::Contains(kProductCodeAddress, size)) {
return 0; return 0;
} }
std::memcpy(Memory::GetPointer(kProductCodeAddress, sizeof(kProductCode)), std::memcpy(Memory::GetPointer(kProductCodeAddress, size),
kProductCode, sizeof(kProductCode)); productCode.c_str(), size);
return kProductCodeAddress; return kProductCodeAddress;
} }
@@ -94,9 +109,8 @@ PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddr
extern "C" uint32_t SCGetProductGameRegion_HLE() extern "C" uint32_t SCGetProductGameRegion_HLE()
{ {
// The PAL setting.txt GAME value is "EU". The SDK's own lookup table at return LookupProductRegion(0x8029CEF8u, 4, 4,
// 0x8029CEF8 maps JP=0, US=1, EU=2. RuntimeConsoleIdentity::Current().gameRegion);
return kPalProductRegion;
} }
PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ());
+65
View File
@@ -0,0 +1,65 @@
#include "nand_settings.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
static void Require(bool condition) {
if (!condition) {
throw std::runtime_error("NAND settings check failed");
}
}
int main() {
const auto root = std::filesystem::temp_directory_path() /
("wiicomp-nand-settings-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()));
const auto path = root / "title/00000001/00000002/data/setting.txt";
try {
Require(!RuntimeNandSettings::Read(root));
Require(!std::filesystem::exists(root));
std::filesystem::create_directories(path.parent_path());
const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n";
std::array<uint8_t, 256> fixture{};
for (size_t i = 0; i < fixture.size(); ++i) {
const unsigned shift = i % 32;
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
fixture[i] = static_cast<uint8_t>(key) ^ (i < plain.size() ? plain[i] : 0);
}
{
std::ofstream output(path, std::ios::binary);
output.write(reinterpret_cast<const char*>(fixture.data()), fixture.size());
}
auto settings = RuntimeNandSettings::Read(root);
Require(settings && RuntimeNandSettings::HasIdentity(*settings));
Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU");
Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US");
std::array<uint8_t, 256> after{};
{
std::ifstream input(path, std::ios::binary);
input.read(reinterpret_cast<char*>(after.data()), after.size());
}
Require(after == fixture);
for (const auto serial : {"", "000000000", "1234567890", "123ABC789"}) {
(*settings)["SERNO"] = serial;
Require(!RuntimeNandSettings::HasIdentity(*settings));
}
(*settings)["SERNO"] = "012345678";
Require(RuntimeNandSettings::HasIdentity(*settings));
(*settings)["CODE"] = "TOOLONG";
Require(!RuntimeNandSettings::HasIdentity(*settings));
(*settings)["CODE"] = "LEH";
settings->erase("GAME");
Require(!RuntimeNandSettings::HasIdentity(*settings));
std::filesystem::resize_file(path, 128);
Require(!RuntimeNandSettings::Read(root));
std::filesystem::remove_all(root);
std::cout << "NAND settings checks passed\n";
return 0;
} catch (const std::exception& error) {
std::filesystem::remove_all(root);
std::cerr << error.what() << '\n';
return 1;
}
}