From 3d39887d9cdcbc27fcab5c102b30a06e9372739a Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 4 May 2025 13:42:43 -0400 Subject: [PATCH] Add function to encrypt steam tickets I also added unit tests to verify this works exactly 1-to-1 to XIVQuickLauncher's implementation. The next step is hooking up all of our things together! --- autotests/CMakeLists.txt | 12 +++++ autotests/crtrandtest.cpp | 44 ++++++++++++++++++ autotests/encryptedargtest.cpp | 43 ++++++++++++++++++ external/libphysis | 2 +- launcher/CMakeLists.txt | 4 +- launcher/include/crtrand.h | 14 ++++++ launcher/include/encryptedarg.h | 3 +- launcher/src/crtrand.cpp | 12 +++++ launcher/src/encryptedarg.cpp | 80 ++++++++++++++++++++++++++++++++- launcher/src/steamapi.cpp | 8 ++-- 10 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 autotests/crtrandtest.cpp create mode 100644 autotests/encryptedargtest.cpp create mode 100644 launcher/include/crtrand.h create mode 100644 launcher/src/crtrand.cpp diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 376173c..d496edc 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -24,3 +24,15 @@ ecm_add_test(utilitytest.cpp LINK_LIBRARIES astra_static Qt::Test NAME_PREFIX "astra-" ) + +ecm_add_test(crtrandtest.cpp + TEST_NAME crtrandtest + LINK_LIBRARIES astra_static Qt::Test + NAME_PREFIX "astra-" +) + +ecm_add_test(encryptedargtest.cpp + TEST_NAME encryptedargtest + LINK_LIBRARIES astra_static Qt::Test + NAME_PREFIX "astra-" +) diff --git a/autotests/crtrandtest.cpp b/autotests/crtrandtest.cpp new file mode 100644 index 0000000..0821714 --- /dev/null +++ b/autotests/crtrandtest.cpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2025 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "crtrand.h" + +class CrtRandTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void randomSeed_data() + { + QTest::addColumn("seed"); + QTest::addColumn("value1"); + QTest::addColumn("value2"); + QTest::addColumn("value3"); + QTest::addColumn("value4"); + + QTest::addRow("test 1") << static_cast(5050) << static_cast(16529) << static_cast(23363) << static_cast(25000) + << static_cast(18427); + QTest::addRow("test 2") << static_cast(19147) << static_cast(29796) << static_cast(24416) << static_cast(1377) + << static_cast(24625); + } + + void randomSeed() + { + QFETCH(uint32_t, seed); + QFETCH(uint32_t, value1); + QFETCH(uint32_t, value2); + QFETCH(uint32_t, value3); + QFETCH(uint32_t, value4); + + auto crtRand = CrtRand(seed); + QCOMPARE(crtRand.next(), value1); + QCOMPARE(crtRand.next(), value2); + QCOMPARE(crtRand.next(), value3); + QCOMPARE(crtRand.next(), value4); + } +}; + +QTEST_MAIN(CrtRandTest) +#include "crtrandtest.moc" diff --git a/autotests/encryptedargtest.cpp b/autotests/encryptedargtest.cpp new file mode 100644 index 0000000..7b3a933 --- /dev/null +++ b/autotests/encryptedargtest.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2025 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "encryptedarg.h" + +class EncryptedArgTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void steamTicket_data() + { + QTest::addColumn("ticketBytes"); + QTest::addColumn("time"); + QTest::addColumn("encryptedTicket"); + + QTest::addRow("test 1") << QByteArrayLiteral( + "caf59103e80d600b4a9233358c131a437cedcf8802eb705223cce84278e6cb4c3655a7accd98097c06748b33d340f9de79920754616e295b88b833d9d84412ffc6a4406c60a6691ce5" + "2c27b5b1c90d36a6a810cef4c81fdc3c75aaa87353433f2f3c7232c00d1198a79bb27df6edb89c5fd0a3a11e957c40") + << static_cast(1746386404) + << QStringLiteral( + "jX6TeGNIJFgecYIQU4YLSnhkbCpTihoIBB6Dw5iw7rAWQiULVB-NhOsfJwLh7EHW5wYwU1XRJAy5dw6JCWQ6v0R5SwP_84DIVDT5gfET_" + "BoXmUr7rfXtJF62vpZ16XfwH2-P2KlaVRTaSYQ8xqTqC_fef6agOdYHvL_g-cNyjjv3xTMsekWnDCDhdG3Y1NvnPcXdAX9v0pjHUu5W5-" + "k8iZhUeTcTjYhMODBPNXz6uf7mKd1wkwAsTnDZoL89k8U1asq78hyaiWsFb4Nb39vK0n0TZfb20yFoXZh9NRO2VpgS,dSTy86oaqm2ZjKu7FnnHmFU6R_" + "lS8pk8EB6itIekPAfC37LYSBIEI1rT9cEvoXNkX6hIGnZ5sthiNPoi5nx9_HdjWYQ9R0Kar-Bgjeu77Av753T0GpVhJhYFyBkMe-" + "NAqGMEjkYFjRUwOX9pEJGEszEL3_mWEbryQ8wL4ZaYk5xu4HAXe5hRwk-JUv__BW8IpiR_OsphQUgeKtRmXUPw1eIU2NYdsd3AJLAP3tiiENaplJ_y8X_" + "OmC8tzvKCCdXapsas3-Won2R_ryQVJlB9j1tAARpNanIEwhOf9CHmbsYM,-8tTNfrKABIfOSlCdr2ajhLqF1i4hRiM-jSzISXAEmc-Nj75Rrg*"); + } + + void steamTicket() + { + QFETCH(QByteArray, ticketBytes); + QFETCH(uint32_t, time); + QFETCH(QString, encryptedTicket); + + QCOMPARE(encryptSteamTicket(ticketBytes, time), encryptedTicket); + } +}; + +QTEST_MAIN(EncryptedArgTest) +#include "encryptedargtest.moc" diff --git a/external/libphysis b/external/libphysis index 806395e..7fff41a 160000 --- a/external/libphysis +++ b/external/libphysis @@ -1 +1 @@ -Subproject commit 806395eb4c6e76f9fbe8fb7ed4e2d864bdb1c522 +Subproject commit 7fff41a808ce28553d4e16c6b3d0d9c9c5f3832d diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a83c722..83261ea 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -51,6 +51,7 @@ target_sources(astra_static PRIVATE include/processlogger.h include/squareenixlogin.h include/steamapi.h + include/crtrand.h src/accountmanager.cpp src/assetupdater.cpp @@ -70,7 +71,8 @@ target_sources(astra_static PRIVATE src/patcher.cpp src/processlogger.cpp src/squareenixlogin.cpp - src/steamapi.cpp) + src/steamapi.cpp + src/crtrand.cpp) target_include_directories(astra_static PUBLIC include) target_link_libraries(astra_static PUBLIC physis diff --git a/launcher/include/crtrand.h b/launcher/include/crtrand.h new file mode 100644 index 0000000..db5f670 --- /dev/null +++ b/launcher/include/crtrand.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +class CrtRand +{ +public: + explicit CrtRand(uint32_t seed); + + uint32_t next(); + +private: + uint32_t seed; +}; diff --git a/launcher/include/encryptedarg.h b/launcher/include/encryptedarg.h index 9317503..2aa7a1d 100644 --- a/launcher/include/encryptedarg.h +++ b/launcher/include/encryptedarg.h @@ -5,4 +5,5 @@ #include -QString encryptGameArg(const QString &arg); \ No newline at end of file +QString encryptGameArg(const QString &arg); +QString encryptSteamTicket(const QByteArray &ticket, uint32_t time); diff --git a/launcher/src/crtrand.cpp b/launcher/src/crtrand.cpp new file mode 100644 index 0000000..4f4f114 --- /dev/null +++ b/launcher/src/crtrand.cpp @@ -0,0 +1,12 @@ +#include "crtrand.h" + +CrtRand::CrtRand(const uint32_t seed) +{ + this->seed = seed; +} + +uint32_t CrtRand::next() +{ + this->seed = 0x343FD * this->seed + 0x269EC3; + return ((this->seed >> 16) & 0xFFFF) & 0x7FFF; +} diff --git a/launcher/src/encryptedarg.cpp b/launcher/src/encryptedarg.cpp index e7cc57b..80440e8 100644 --- a/launcher/src/encryptedarg.cpp +++ b/launcher/src/encryptedarg.cpp @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "encryptedarg.h" +#include "crtrand.h" +#include #include #if defined(Q_OS_MAC) @@ -83,4 +85,80 @@ QString encryptGameArg(const QString &arg) physis_blowfish_free(blowfish); return QStringLiteral("//**sqex0003%1%2**//").arg(base64, QString(QLatin1Char(checksum))); -} \ No newline at end of file +} + +// Based off of the XIVQuickLauncher implementation +constexpr auto SQEX_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"; +constexpr int SPLIT_SIZE = 300; + +QStringList intoChunks(const QString &str, const int maxChunkSize) +{ + QStringList chunks; + for (int i = 0; i < str.length(); i += maxChunkSize) { + chunks.push_back(str.mid(i, std::min(static_cast(maxChunkSize), str.length() - i))); + } + + return chunks; +} + +QString encryptSteamTicket(const QByteArray &ticket, uint32_t time) +{ + // Round the time down + time -= 5; + time -= time % 60; + + auto ticketString = QString::fromLatin1(ticket.toHex()).remove(QLatin1Char('-')).toLower(); + auto rawTicketBytes = ticketString.toLatin1(); + rawTicketBytes.append('\0'); + + ushort ticketSum = 0; + for (const auto b : rawTicketBytes) { + ticketSum += b; + } + + QByteArray binaryWriter; + binaryWriter.append(reinterpret_cast(&ticketSum), sizeof(ushort)); + binaryWriter.append(rawTicketBytes); + + const int castTicketSum = static_cast(ticketSum); + const auto seed = time ^ castTicketSum; + auto rand = CrtRand(seed); + + const auto numRandomBytes = (static_cast(rawTicketBytes.length() + 9) & 0xFFFFFFFFFFFFFFF8) - 2 - static_cast(rawTicketBytes.length()); + auto garbage = QByteArray(); + garbage.resize(numRandomBytes); + + uint badSum = *reinterpret_cast(binaryWriter.data()); + + for (auto i = 0u; i < numRandomBytes; i++) { + const auto randChar = SQEX_ALPHABET[static_cast(badSum + rand.next()) & 0x3F]; + garbage[i] = static_cast(randChar); + badSum += randChar; + } + + binaryWriter.append(garbage); + + char blowfishKey[17]{}; + sprintf(blowfishKey, "%08x#un@e=x>", time); + + binaryWriter.remove(0, 4); + binaryWriter.insert(0, reinterpret_cast(&badSum), sizeof(uint)); + + // swap first two bytes + auto finalBytes = binaryWriter; + std::swap(finalBytes[0], finalBytes[1]); + + SteamTicketBlowfish *blowfish = miscel_steamticket_blowfish_initialize(reinterpret_cast(blowfishKey), 16); + + miscel_steamticket_blowfish_encrypt(blowfish, reinterpret_cast(finalBytes.data()), finalBytes.size()); + Q_ASSERT(finalBytes.length() % 8 == 0); + + miscel_steamticket_physis_blowfish_free(blowfish); + + auto encoded = finalBytes.toBase64(QByteArray::Base64Option::Base64UrlEncoding | QByteArray::Base64Option::KeepTrailingEquals); + encoded.replace('+', '-'); + encoded.replace('/', '_'); + encoded.replace('=', '*'); + + return intoChunks(QString::fromLatin1(encoded), SPLIT_SIZE).join(QLatin1Char(',')); +} diff --git a/launcher/src/steamapi.cpp b/launcher/src/steamapi.cpp index fdba45c..de5f978 100644 --- a/launcher/src/steamapi.cpp +++ b/launcher/src/steamapi.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "steamapi.h" +#include "encryptedarg.h" #include "launchercore.h" #include @@ -41,7 +42,8 @@ QCoro::Task SteamAPI::getTicket() url.setPort(50481); url.setPath(QStringLiteral("/ticket")); - auto reply = co_await m_qnam.get(QNetworkRequest(url)); + const auto reply = co_await m_qnam.get(QNetworkRequest(url)); + const auto ticketBytes = reply->readAll(); - co_return QStringLiteral("todo"); -} \ No newline at end of file + co_return encryptSteamTicket(ticketBytes, 5); // TOOD: get time +}