From c755767b6d9291384ed25491ff0e9f0260127e93 Mon Sep 17 00:00:00 2001 From: 9001-Sols <9001.sols@gmail.com> Date: Wed, 11 Feb 2026 08:24:49 -0500 Subject: [PATCH] Trust Tokens --- CMakeLists.txt | 2 + src/command_handler.h | 32 ++++++- src/helpers.h | 9 +- src/main.cpp | 44 ++++++---- src/menus.h | 129 ++++++++++++++++++++++++---- src/network.cpp | 34 +++++++- src/trust_token.cpp | 195 ++++++++++++++++++++++++++++++++++++++++++ src/trust_token.h | 30 +++++++ src/xiloader.rc.in | 4 +- 9 files changed, 439 insertions(+), 40 deletions(-) create mode 100644 src/trust_token.cpp create mode 100644 src/trust_token.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d5dbe2..8481e3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,8 @@ add_executable(xiloader src/network.cpp src/network.h src/polcore.h + src/trust_token.cpp + src/trust_token.h ${CMAKE_CURRENT_BINARY_DIR}/src/xiloader.rc xiloader.manifest ) diff --git a/src/command_handler.h b/src/command_handler.h index ec0a122..a17999f 100644 --- a/src/command_handler.h +++ b/src/command_handler.h @@ -1,7 +1,7 @@ /* =========================================================================== - Copyright (c) 2025 LandSandBoat Dev Teams + Copyright (c) 2026 LandSandBoat Dev Teams This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -38,11 +38,14 @@ namespace globals extern char* g_CharacterList; extern bool g_IsRunning; extern bool g_FirstLogin; + extern std::string g_TrustToken; + extern bool g_TrustThisComputer; } // namespace globals #include "defines.h" #include "helpers.h" #include "network.h" +#include "trust_token.h" #include #include @@ -81,6 +84,19 @@ bool handleLoginCommand(int8_t command, json& login_reply_json, uint32_t& accoun xiloader::console::output(xiloader::color::success, "Successfully logged in as %s!", globals::g_Username.c_str()); + auto maybeTrustToken = jsonGet(login_reply_json, "trust_token"); + if (maybeTrustToken.has_value() && !maybeTrustToken.value().empty()) + { + // Use server-provided expiry; fall back to 30 days if absent + int64_t expires = jsonGet(login_reply_json, "trust_expires") + .value_or(static_cast(time(nullptr)) + (30 * 24 * 60 * 60)); + saveTrustToken(globals::g_ServerAddress, globals::g_Username, + maybeTrustToken.value(), expires); + int64_t days = (expires - static_cast(time(nullptr))) / (24 * 60 * 60); + xiloader::console::output(xiloader::color::info, "This computer is now trusted for %lld day%s.", + days, days == 1 ? "" : "s"); + } + shutdown(sock, SD_BOTH); return true; @@ -133,6 +149,18 @@ bool handleLoginCommand(int8_t command, json& login_reply_json, uint32_t& accoun return false; } + // LOGIN_ERROR_TRUST_TOKEN_INVALID + case 0x0013: + { + removeTrustToken(globals::g_ServerAddress, globals::g_Username); + xiloader::console::output(xiloader::color::error, "=========================================================="); + xiloader::console::output(xiloader::color::error, "Trust token rejected! It has been cleared from this computer."); + xiloader::console::output(xiloader::color::error, "Please log in again and enter your OTP code."); + xiloader::console::output(xiloader::color::error, "=========================================================="); + + return false; + } + // LOGIN_SUCCESS_CREATE_TOTP case 0x0010: { @@ -194,6 +222,8 @@ bool handleLoginCommand(int8_t command, json& login_reply_json, uint32_t& accoun xiloader::console::output(xiloader::color::info, "Your TOTP has been removed."); xiloader::console::output(xiloader::color::info, "You no longer need to use an OTP code to login."); + removeTrustToken(globals::g_ServerAddress, globals::g_Username); + return false; } diff --git a/src/helpers.h b/src/helpers.h index 8276cd0..19e6b56 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -1,7 +1,7 @@ /* =========================================================================== -Copyright (c) 2022 LandSandBoat Dev Teams +Copyright (c) 2026 LandSandBoat Dev Teams This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,6 +20,13 @@ along with this program. If not, see http://www.gnu.org/licenses/ */ #pragma once +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include +#include + #include #include #include diff --git a/src/main.cpp b/src/main.cpp index 6db92d8..ef8f197 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,7 @@ =========================================================================== Copyright (c) 2010-2015 Darkstar Dev Teams -Copyright (c) 2021-2022 LandSandBoat Dev Teams +Copyright (c) 2021-2026 LandSandBoat Dev Teams This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -45,19 +45,21 @@ using json = nlohmann::json; /* Global Variables */ namespace globals { - xiloader::Language g_Language = xiloader::Language::English; // The language of the loader to be used for polcore. - std::string g_ServerAddress = "127.0.0.1"; // The server address to connect to. - uint16_t g_ServerPort = 51220; // The server lobby server port to connect to. - uint16_t g_LoginDataPort = 54230; // Login server data port to connect to - uint16_t g_LoginViewPort = 54001; // Login view port to connect to - uint16_t g_LoginAuthPort = 54231; // Login auth port to connect to - std::string g_Username = ""; // The username being logged in with. - std::string g_Password = ""; // The password being logged in with. - std::string g_OtpCode = ""; // The OTP code the user input - char g_SessionHash[16] = {}; // Session hash sent from auth - std::string g_Email = ""; // Email, currently unused - std::array g_VersionNumber = { 2, 0, 1 }; // xiloader version number sent to auth server. Must be x.x.x with single characters for 'x'. Remember to also change in xiloader.rc.in - bool g_FirstLogin = false; // set to true when --user --pass are both set to allow for autologin + xiloader::Language g_Language = xiloader::Language::English; // The language of the loader to be used for polcore. + std::string g_ServerAddress = "127.0.0.1"; // The server address to connect to. + uint16_t g_ServerPort = 51220; // The server lobby server port to connect to. + uint16_t g_LoginDataPort = 54230; // Login server data port to connect to + uint16_t g_LoginViewPort = 54001; // Login view port to connect to + uint16_t g_LoginAuthPort = 54231; // Login auth port to connect to + std::string g_Username = ""; // The username being logged in with. + std::string g_Password = ""; // The password being logged in with. + std::string g_OtpCode = ""; // The OTP code the user input + char g_SessionHash[16] = {}; // Session hash sent from auth + std::string g_Email = ""; // Email, currently unused + std::array g_VersionNumber = { 2, 1, 0 }; // xiloader version number sent to auth server. Must be x.x.x with single characters for 'x'. Remember to also change in xiloader.rc.in + bool g_FirstLogin = false; // set to true when --user --pass are both set to allow for autologin + std::string g_TrustToken = ""; // trust token loaded from disk or received from server + bool g_TrustThisComputer = false; // user checkbox / CLI flag for "trust this computer" char* g_CharacterList = NULL; // Pointer to the character list data being sent from the server. bool g_IsRunning = false; // Flag to determine if the network threads should hault. @@ -467,6 +469,11 @@ int __cdecl main(int argc, char* argv[]) .help("(optional) Determines whether or not to hide the console window after FFXI starts.") .append(); + args.add_argument("--trust") + .implicit_value(true) + .help("(optional) Trust this computer for 30 days, skipping 2FA on subsequent logins.") + .append(); + args.add_argument("--json", "--json-file") .help("(optional) The json file to load arguments in from") .append(); @@ -531,6 +538,8 @@ int __cdecl main(int argc, char* argv[]) globals::g_Hide = args.is_used("--hide") ? args.get("--hide") : globals::g_Hide; + globals::g_TrustThisComputer = args.is_used("--trust") ? args.get("--trust") : globals::g_TrustThisComputer; + bool readInJsonArgs = false; if (!jsonFilename.empty()) { @@ -589,8 +598,9 @@ int __cdecl main(int argc, char* argv[]) globals::g_OtpCode = jsonGet(jsonData, "otp").value_or(globals::g_OtpCode); globals::g_Email = jsonGet(jsonData, "email").value_or(globals::g_Email); - bUseHairpinFix = jsonGet(jsonData, "hairpin").value_or(bUseHairpinFix); - globals::g_Hide = jsonGet(jsonData, "hide").value_or(globals::g_Hide); + bUseHairpinFix = jsonGet(jsonData, "hairpin").value_or(bUseHairpinFix); + globals::g_Hide = jsonGet(jsonData, "hide").value_or(globals::g_Hide); + globals::g_TrustThisComputer = jsonGet(jsonData, "trust_this_computer").value_or(globals::g_TrustThisComputer); std::string language = jsonGet(jsonData, "language").value_or({}); @@ -753,7 +763,7 @@ int __cdecl main(int argc, char* argv[]) { /* Invoke the setup functions for polcore.. */ // Create string for the login view port - std::string polcorecmd = " /game eAZcFcB -net 3 -port " + globals::g_LoginViewPort; + std::string polcorecmd = " /game eAZcFcB -net 3 -port " + std::to_string(globals::g_LoginViewPort); // Cast to an LPSTR LPSTR cmd = const_cast(polcorecmd.c_str()); polcore->SetAreaCode(globals::g_Language); diff --git a/src/menus.h b/src/menus.h index 222b505..68d3fe7 100644 --- a/src/menus.h +++ b/src/menus.h @@ -1,7 +1,7 @@ /* =========================================================================== - Copyright (c) 2025 LandSandBoat Dev Teams + Copyright (c) 2026 LandSandBoat Dev Teams This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,6 +26,8 @@ #include "ftxui/component/component_options.hpp" // for MenuOption #include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive +#include "trust_token.h" + enum class MenuSelection : uint8_t { None = 0, @@ -37,6 +39,7 @@ enum class MenuSelection : uint8_t RemoveTwoFactorOTP = 6, RegenerateTwoFactorRemovalCode = 7, ValidateTwoFactorOTP = 8, + RevokeComputerTrust = 9, Exit = 255, }; @@ -56,7 +59,8 @@ namespace menus MenuEntry("2) Remove 2FA OTP"), MenuEntry("3) Regenerate 2FA OTP removal code"), MenuEntry("4) Validate 2FA OTP"), - MenuEntry("5) Exit Menu"), + MenuEntry("5) Revoke Computer Trust"), + MenuEntry("6) Exit Menu"), }, &selected); @@ -88,12 +92,18 @@ namespace menus screen.Exit(); return true; } - else if (event == Event::Character('5')) // Select "Exit Menu" + else if (event == Event::Character('5')) // Select "Revoke Computer Trust" { selected = 4; screen.Exit(); return true; } + else if (event == Event::Character('6')) // Select "Exit Menu" + { + selected = 5; + screen.Exit(); + return true; + } else if (event == event.Return) { screen.Exit(); @@ -126,6 +136,10 @@ namespace menus return MenuSelection::ValidateTwoFactorOTP; } case 4: + { + return MenuSelection::RevokeComputerTrust; + } + case 5: default: { return MenuSelection::None; // In this instance, the caller will replay the main menu @@ -229,7 +243,7 @@ namespace menus return MenuSelection::None; } - void enterCredentialsWithOTP(std::string& username, std::string& password, std::string& OTP) + void enterCredentialsWithOTP(std::string& username, std::string& password, std::string& OTP, bool* trustThisComputer = nullptr, const std::string& serverAddress = "") { ftxui::InputOption password_option; password_option.password = true; @@ -238,15 +252,23 @@ namespace menus ftxui::Component input_password = ftxui::Input(&password, "", password_option); ftxui::Component input_otp = ftxui::Input(&OTP, ""); - // The component tree: - auto component = ftxui::Container::Vertical({ - input_username, - input_password, - input_otp, - }); + std::vector components = { input_username, input_password, input_otp }; + + ftxui::Component checkbox_trust; + if (trustThisComputer) + { + checkbox_trust = ftxui::Checkbox("Trust this computer", trustThisComputer); + components.push_back(checkbox_trust); + } + + auto component = ftxui::Container::Vertical(components); auto screen = ftxui::ScreenInteractive::TerminalOutput(); + // Trust token cache — only re-check when username changes + std::string lastCheckedUser; + bool isTrusted = false; + // clang-format off component |= ftxui::CatchEvent([&](ftxui::Event event) { @@ -258,9 +280,27 @@ namespace menus } else if (input_password->Focused()) { - input_otp->TakeFocus(); + if (isTrusted) + { + screen.Exit(); + } + else + { + input_otp->TakeFocus(); + } } else if (input_otp->Focused()) + { + if (checkbox_trust) + { + checkbox_trust->TakeFocus(); + } + else + { + screen.Exit(); + } + } + else if (checkbox_trust && checkbox_trust->Focused()) { screen.Exit(); } @@ -274,10 +314,37 @@ namespace menus // clang-format off auto renderer = ftxui::Renderer(component, [&] { - return ftxui::vbox({ ftxui::hbox(ftxui::text(" Username: "), input_username->Render()), - ftxui::hbox(ftxui::text(" Password: "), input_password->Render()), - ftxui::hbox(ftxui::text(" OTP Code: "), input_otp->Render()) - }) | ftxui::border; + // Check trust status when username changes + if (!serverAddress.empty() && username != lastCheckedUser) + { + lastCheckedUser = username; + isTrusted = !username.empty() && !loadTrustToken(serverAddress, username).empty(); + if (isTrusted) + { + OTP.clear(); + } + } + + auto elements = std::vector{ + ftxui::hbox(ftxui::text(" Username: "), input_username->Render()), + ftxui::hbox(ftxui::text(" Password: "), input_password->Render()), + }; + + if (isTrusted) + { + elements.push_back(ftxui::hbox(ftxui::text(" OTP Code: "), input_otp->Render(), ftxui::text(" (optional - trusted)") | ftxui::color(ftxui::Color::GrayDark))); + elements.push_back(ftxui::hbox(ftxui::text(" Computer is trusted") | ftxui::color(ftxui::Color::Green))); + } + else + { + elements.push_back(ftxui::hbox(ftxui::text(" OTP Code: "), input_otp->Render())); + if (checkbox_trust) + { + elements.push_back(ftxui::hbox(ftxui::text(" "), checkbox_trust->Render())); + } + } + + return ftxui::vbox(elements) | ftxui::border; }); // clang-format on @@ -501,4 +568,36 @@ namespace menus return true; } + void enterUsernameOnly(std::string& username) + { + ftxui::Component input_username = ftxui::Input(&username, ""); + + auto component = ftxui::Container::Vertical({ + input_username, + }); + + auto screen = ftxui::ScreenInteractive::TerminalOutput(); + + // clang-format off + component |= ftxui::CatchEvent([&](ftxui::Event event) + { + if (event == event.Return) + { + screen.Exit(); + return true; + } + return false; + }); + // clang-format on + + // clang-format off + auto renderer = ftxui::Renderer(component, [&] + { + return ftxui::vbox({ ftxui::hbox(ftxui::text(" Username: "), input_username->Render()), + }) | ftxui::border; + }); + // clang-format on + + screen.Loop(renderer); + } } // namespace menus diff --git a/src/network.cpp b/src/network.cpp index 8a37eae..7b6c3df 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2,6 +2,7 @@ =========================================================================== Copyright (c) 2010-2014 Darkstar Dev Teams +Copyright (c) 2026 LandSandBoat Dev Teams This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,6 +25,7 @@ This file is part of DarkStar-server source code. #include "helpers.h" #include "menus.h" #include "network.h" +#include "trust_token.h" #include #include @@ -47,6 +49,8 @@ namespace globals extern char* g_CharacterList; extern bool g_IsRunning; extern bool g_FirstLogin; + extern std::string g_TrustToken; + extern bool g_TrustThisComputer; } // mbed tls state @@ -368,15 +372,19 @@ namespace xiloader } } - globals::g_Username = ""; - globals::g_Password = ""; - globals::g_OtpCode = ""; + globals::g_Username = ""; + globals::g_Password = ""; + globals::g_OtpCode = ""; + globals::g_TrustToken = ""; switch (selected) { case MenuSelection::Login: { - menus::enterCredentialsWithOTP(globals::g_Username, globals::g_Password, globals::g_OtpCode); + menus::enterCredentialsWithOTP(globals::g_Username, globals::g_Password, globals::g_OtpCode, &globals::g_TrustThisComputer, globals::g_ServerAddress); + + // Load trust token for this server+user combination + globals::g_TrustToken = loadTrustToken(globals::g_ServerAddress, globals::g_Username); command = 0x10; // login break; @@ -445,6 +453,15 @@ namespace xiloader command = 0x34; break; } + case MenuSelection::RevokeComputerTrust: + { + std::string revokeUsername; + xiloader::console::output("Enter the username to revoke trust for:"); + menus::enterUsernameOnly(revokeUsername); + removeTrustToken(globals::g_ServerAddress, revokeUsername); + xiloader::console::output(xiloader::color::info, "Computer trust revoked for '%s'.", revokeUsername.c_str()); + return 0; // Return to menu + } case MenuSelection::Exit: { exit(0); // Bit ugly, can't really exit properly with the current code flow @@ -463,6 +480,9 @@ namespace xiloader /* User has auto-login enabled.. */ command = 0x10; globals::g_FirstLogin = false; + + // Load trust token for autologin + globals::g_TrustToken = loadTrustToken(globals::g_ServerAddress, globals::g_Username); } json login_json; @@ -473,6 +493,12 @@ namespace xiloader login_json["version"] = globals::g_VersionNumber; login_json["command"] = command; + if (command == 0x10) // Only send trust fields for login + { + login_json["trust_token"] = globals::g_TrustToken; + login_json["trust_this_computer"] = globals::g_TrustThisComputer; + } + std::string str = login_json.dump(); const char* strBuffer = str.c_str(); size_t strBufferLen = strlen(strBuffer); diff --git a/src/trust_token.cpp b/src/trust_token.cpp new file mode 100644 index 0000000..7c5b59b --- /dev/null +++ b/src/trust_token.cpp @@ -0,0 +1,195 @@ +/* +=========================================================================== + +Copyright (c) 2026 LandSandBoat Dev Teams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#include "trust_token.h" + +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "console.h" + +using json = nlohmann::json; + +namespace +{ + +std::string getTrustTokenDir() +{ + char* appdata = nullptr; + size_t len = 0; + _dupenv_s(&appdata, &len, "APPDATA"); + std::string dir; + if (appdata) + { + dir = std::string(appdata) + "\\xiloader\\"; + free(appdata); + } + else + { + xiloader::console::output(xiloader::color::warning, "APPDATA not set; trust tokens will be stored in the current directory."); + dir = ".\\"; + } + std::filesystem::create_directories(dir); + return dir; +} + +std::string getTrustTokenFilepath() +{ + return getTrustTokenDir() + "trust_tokens.dat"; +} + +std::string buildTrustTokenKey(const std::string& server, const std::string& username) +{ + return server + "|" + username; +} + +json readTokenStore() +{ + std::string filepath = getTrustTokenFilepath(); + if (!std::filesystem::exists(filepath)) + { + return json::object(); + } + + std::ifstream file(filepath, std::ios::binary | std::ios::ate); + if (!file.is_open()) + { + return json::object(); + } + + auto fileSize = file.tellg(); + if (fileSize <= 0) + { + return json::object(); + } + + file.seekg(0, std::ios::beg); + std::vector encrypted(static_cast(fileSize)); + file.read(reinterpret_cast(encrypted.data()), fileSize); + file.close(); + + DATA_BLOB encryptedBlob; + encryptedBlob.pbData = encrypted.data(); + encryptedBlob.cbData = static_cast(encrypted.size()); + + DATA_BLOB decryptedBlob; + if (!CryptUnprotectData(&encryptedBlob, nullptr, nullptr, nullptr, nullptr, 0, &decryptedBlob)) + { + return json::object(); + } + + std::string jsonStr(reinterpret_cast(decryptedBlob.pbData), decryptedBlob.cbData); + LocalFree(decryptedBlob.pbData); + + json tokenData = json::parse(jsonStr, nullptr, false); + if (tokenData.is_discarded()) + { + return json::object(); + } + + return tokenData; +} + +void writeTokenStore(const json& tokenData) +{ + std::string filepath = getTrustTokenFilepath(); + std::string jsonStr = tokenData.dump(); + + DATA_BLOB plainBlob; + plainBlob.pbData = reinterpret_cast(jsonStr.data()); + plainBlob.cbData = static_cast(jsonStr.size()); + + DATA_BLOB encryptedBlob; + if (CryptProtectData(&plainBlob, nullptr, nullptr, nullptr, nullptr, 0, &encryptedBlob)) + { + std::ofstream file(filepath, std::ios::binary | std::ios::trunc); + file.write(reinterpret_cast(encryptedBlob.pbData), encryptedBlob.cbData); + file.close(); + LocalFree(encryptedBlob.pbData); + } +} + +} // anonymous namespace + +std::string loadTrustToken(const std::string& server, const std::string& username) +{ + json tokenData = readTokenStore(); + + std::string key = buildTrustTokenKey(server, username); + if (!tokenData.contains(key)) + { + return ""; + } + + auto& entry = tokenData[key]; + if (!entry.contains("token") || !entry.contains("expires")) + { + return ""; + } + + int64_t expires = entry["expires"].get(); + if (static_cast(time(nullptr)) >= expires) + { + // Token expired client-side, remove it + tokenData.erase(key); + writeTokenStore(tokenData); + return ""; + } + + return entry["token"].get(); +} + +void saveTrustToken(const std::string& server, const std::string& username, + const std::string& token, int64_t expiresEpoch) +{ + json tokenData = readTokenStore(); + + std::string key = buildTrustTokenKey(server, username); + tokenData[key] = { { "token", token }, { "expires", expiresEpoch } }; + + writeTokenStore(tokenData); +} + +void removeTrustToken(const std::string& server, const std::string& username) +{ + json tokenData = readTokenStore(); + + std::string key = buildTrustTokenKey(server, username); + if (!tokenData.contains(key)) + { + return; + } + + tokenData.erase(key); + writeTokenStore(tokenData); +} diff --git a/src/trust_token.h b/src/trust_token.h new file mode 100644 index 0000000..440b114 --- /dev/null +++ b/src/trust_token.h @@ -0,0 +1,30 @@ +/* +=========================================================================== + +Copyright (c) 2026 LandSandBoat Dev Teams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include +#include + +std::string loadTrustToken(const std::string& server, const std::string& username); +void saveTrustToken(const std::string& server, const std::string& username, + const std::string& token, int64_t expiresEpoch); +void removeTrustToken(const std::string& server, const std::string& username); diff --git a/src/xiloader.rc.in b/src/xiloader.rc.in index fe97f95..c14735a 100644 --- a/src/xiloader.rc.in +++ b/src/xiloader.rc.in @@ -3,13 +3,13 @@ // VALUE "FileVersion", ... 1 VERSIONINFO -FILEVERSION 2,0,1 +FILEVERSION 2,1,0 BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "081604b0" BEGIN - VALUE "FileVersion", "2.0.1" + VALUE "FileVersion", "2.1.0" END END END