mirror of
https://github.com/redstrate/Astra.git
synced 2025-05-17 14:47: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:
parent
7065cc041b
commit
3d39887d9c
10 changed files with 215 additions and 7 deletions
|
@ -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
44
autotests/crtrandtest.cpp
Normal 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"
|
43
autotests/encryptedargtest.cpp
Normal file
43
autotests/encryptedargtest.cpp
Normal 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
2
external/libphysis
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 806395eb4c6e76f9fbe8fb7ed4e2d864bdb1c522
|
||||
Subproject commit 7fff41a808ce28553d4e16c6b3d0d9c9c5f3832d
|
|
@ -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
|
||||
|
|
14
launcher/include/crtrand.h
Normal file
14
launcher/include/crtrand.h
Normal file
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class CrtRand
|
||||
{
|
||||
public:
|
||||
explicit CrtRand(uint32_t seed);
|
||||
|
||||
uint32_t next();
|
||||
|
||||
private:
|
||||
uint32_t seed;
|
||||
};
|
|
@ -6,3 +6,4 @@
|
|||
#include <QString>
|
||||
|
||||
QString encryptGameArg(const QString &arg);
|
||||
QString encryptSteamTicket(const QByteArray &ticket, uint32_t time);
|
||||
|
|
12
launcher/src/crtrand.cpp
Normal file
12
launcher/src/crtrand.cpp
Normal 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;
|
||||
}
|
|
@ -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)
|
||||
|
@ -84,3 +86,79 @@ QString encryptGameArg(const QString &arg)
|
|||
|
||||
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(','));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Add table
Reference in a new issue