1
Fork 0
mirror of https://github.com/redstrate/Astra.git synced 2025-05-17 06:37:45 +00:00

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!
This commit is contained in:
Joshua Goins 2025-05-04 13:42:43 -04:00
parent 7065cc041b
commit 3d39887d9c
10 changed files with 215 additions and 7 deletions

View file

@ -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-"
)

44
autotests/crtrandtest.cpp Normal file
View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QtTest/QtTest>
#include "crtrand.h"
class CrtRandTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void randomSeed_data()
{
QTest::addColumn<uint32_t>("seed");
QTest::addColumn<uint32_t>("value1");
QTest::addColumn<uint32_t>("value2");
QTest::addColumn<uint32_t>("value3");
QTest::addColumn<uint32_t>("value4");
QTest::addRow("test 1") << static_cast<uint32_t>(5050) << static_cast<uint32_t>(16529) << static_cast<uint32_t>(23363) << static_cast<uint32_t>(25000)
<< static_cast<uint32_t>(18427);
QTest::addRow("test 2") << static_cast<uint32_t>(19147) << static_cast<uint32_t>(29796) << static_cast<uint32_t>(24416) << static_cast<uint32_t>(1377)
<< static_cast<uint32_t>(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"

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QtTest/QtTest>
#include "encryptedarg.h"
class EncryptedArgTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void steamTicket_data()
{
QTest::addColumn<QByteArray>("ticketBytes");
QTest::addColumn<uint32_t>("time");
QTest::addColumn<QString>("encryptedTicket");
QTest::addRow("test 1") << QByteArrayLiteral(
"caf59103e80d600b4a9233358c131a437cedcf8802eb705223cce84278e6cb4c3655a7accd98097c06748b33d340f9de79920754616e295b88b833d9d84412ffc6a4406c60a6691ce5"
"2c27b5b1c90d36a6a810cef4c81fdc3c75aaa87353433f2f3c7232c00d1198a79bb27df6edb89c5fd0a3a11e957c40")
<< static_cast<uint32_t>(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"

2
external/libphysis vendored

@ -1 +1 @@
Subproject commit 806395eb4c6e76f9fbe8fb7ed4e2d864bdb1c522
Subproject commit 7fff41a808ce28553d4e16c6b3d0d9c9c5f3832d

View file

@ -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

View file

@ -0,0 +1,14 @@
#pragma once
#include <cstdint>
class CrtRand
{
public:
explicit CrtRand(uint32_t seed);
uint32_t next();
private:
uint32_t seed;
};

View file

@ -5,4 +5,5 @@
#include <QString>
QString encryptGameArg(const QString &arg);
QString encryptGameArg(const QString &arg);
QString encryptSteamTicket(const QByteArray &ticket, uint32_t time);

12
launcher/src/crtrand.cpp Normal file
View file

@ -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;
}

View file

@ -2,7 +2,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "encryptedarg.h"
#include "crtrand.h"
#include <QDebug>
#include <physis.hpp>
#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)));
}
}
// 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<qlonglong>(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<const char *>(&ticketSum), sizeof(ushort));
binaryWriter.append(rawTicketBytes);
const int castTicketSum = static_cast<short>(ticketSum);
const auto seed = time ^ castTicketSum;
auto rand = CrtRand(seed);
const auto numRandomBytes = (static_cast<ulong>(rawTicketBytes.length() + 9) & 0xFFFFFFFFFFFFFFF8) - 2 - static_cast<ulong>(rawTicketBytes.length());
auto garbage = QByteArray();
garbage.resize(numRandomBytes);
uint badSum = *reinterpret_cast<uint32_t *>(binaryWriter.data());
for (auto i = 0u; i < numRandomBytes; i++) {
const auto randChar = SQEX_ALPHABET[static_cast<int>(badSum + rand.next()) & 0x3F];
garbage[i] = static_cast<char>(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<const char *>(&badSum), sizeof(uint));
// swap first two bytes
auto finalBytes = binaryWriter;
std::swap(finalBytes[0], finalBytes[1]);
SteamTicketBlowfish *blowfish = miscel_steamticket_blowfish_initialize(reinterpret_cast<uint8_t *>(blowfishKey), 16);
miscel_steamticket_blowfish_encrypt(blowfish, reinterpret_cast<uint8_t *>(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(','));
}

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "steamapi.h"
#include "encryptedarg.h"
#include "launchercore.h"
#include <QCoroNetwork>
@ -41,7 +42,8 @@ QCoro::Task<QString> 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");
}
co_return encryptSteamTicket(ticketBytes, 5); // TOOD: get time
}