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
|
LINK_LIBRARIES astra_static Qt::Test
|
||||||
NAME_PREFIX "astra-"
|
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/processlogger.h
|
||||||
include/squareenixlogin.h
|
include/squareenixlogin.h
|
||||||
include/steamapi.h
|
include/steamapi.h
|
||||||
|
include/crtrand.h
|
||||||
|
|
||||||
src/accountmanager.cpp
|
src/accountmanager.cpp
|
||||||
src/assetupdater.cpp
|
src/assetupdater.cpp
|
||||||
|
@ -70,7 +71,8 @@ target_sources(astra_static PRIVATE
|
||||||
src/patcher.cpp
|
src/patcher.cpp
|
||||||
src/processlogger.cpp
|
src/processlogger.cpp
|
||||||
src/squareenixlogin.cpp
|
src/squareenixlogin.cpp
|
||||||
src/steamapi.cpp)
|
src/steamapi.cpp
|
||||||
|
src/crtrand.cpp)
|
||||||
target_include_directories(astra_static PUBLIC include)
|
target_include_directories(astra_static PUBLIC include)
|
||||||
target_link_libraries(astra_static PUBLIC
|
target_link_libraries(astra_static PUBLIC
|
||||||
physis
|
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>
|
#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
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
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include "encryptedarg.h"
|
#include "encryptedarg.h"
|
||||||
|
#include "crtrand.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
#include <physis.hpp>
|
#include <physis.hpp>
|
||||||
|
|
||||||
#if defined(Q_OS_MAC)
|
#if defined(Q_OS_MAC)
|
||||||
|
@ -84,3 +86,79 @@ QString encryptGameArg(const QString &arg)
|
||||||
|
|
||||||
return QStringLiteral("//**sqex0003%1%2**//").arg(base64, QString(QLatin1Char(checksum)));
|
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
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include "steamapi.h"
|
#include "steamapi.h"
|
||||||
|
#include "encryptedarg.h"
|
||||||
#include "launchercore.h"
|
#include "launchercore.h"
|
||||||
|
|
||||||
#include <QCoroNetwork>
|
#include <QCoroNetwork>
|
||||||
|
@ -41,7 +42,8 @@ QCoro::Task<QString> SteamAPI::getTicket()
|
||||||
url.setPort(50481);
|
url.setPort(50481);
|
||||||
url.setPath(QStringLiteral("/ticket"));
|
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