Skip to content
Permalink
Browse files

POC Mojang auth cert pinning

  • Loading branch information...
peterix committed Apr 2, 2019
1 parent bf93ba0 commit d3ee7244bd07cb49c0ae2de4c60d95fa622e3b90
@@ -2,17 +2,19 @@
#include "net/HttpMetaCache.h"
#include "BaseVersion.h"
#include "BaseVersionList.h"
#include "tasks/Task.h"
#include "meta/Index.h"
#include "FileSystem.h"

#include <QDir>
#include <QCoreApplication>
#include <QNetworkProxy>
#include <QNetworkAccessManager>
#include <QDebug>
#include "tasks/Task.h"
#include "meta/Index.h"
#include "FileSystem.h"
#include <QSslKey>
#include <QNetworkReply>
#include <QDebug>


struct Env::Private
{
QNetworkAccessManager m_qnam;
@@ -4,6 +4,7 @@
#include "icons/IIconList.h"
#include <QString>
#include <QMap>
#include <functional>

#include "multimc_logic_export.h"

@@ -25,7 +25,8 @@ struct MULTIMC_LOGIC_EXPORT AuthSession
Undetermined,
RequiresPassword,
PlayableOffline,
PlayableOnline
PlayableOnline,
SecurityError
} status = Undetermined;

User u;
@@ -18,16 +18,24 @@
#include "MojangAccount.h"
#include "flows/RefreshTask.h"
#include "flows/AuthenticateTask.h"
#include "net/URLConstants.h"

#include <QUuid>
#include <QJsonObject>
#include <QJsonArray>
#include <QRegExp>
#include <QStringList>
#include <QJsonDocument>
#include <QSslKey>
#include <QNetworkReply>

#include <QDebug>

MojangAccount::MojangAccount(QObject* parent) : QObject(parent)
{
connect(&m_auth_mgr, &QNetworkAccessManager::encrypted, this, &MojangAccount::certPinningHandler);
}

MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
{
// The JSON object must at least have a username for it to be valid.
@@ -229,7 +237,16 @@ void MojangAccount::authFailed(QString reason)
auto session = m_currentTask->getAssignedSession();
// This is emitted when the yggdrasil tasks time out or are cancelled.
// -> we treat the error as no-op
if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT)
if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SECURITY)
{
if (session)
{
session->status = AuthSession::SecurityError;
session->auth_server_online = false;
fillSession(session);
}
}
else if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT)
{
if (session)
{
@@ -253,6 +270,24 @@ void MojangAccount::authFailed(QString reason)
m_currentTask.reset();
}

void MojangAccount::certPinningHandler(QNetworkReply* reply)
{
if(!reply->url().host().endsWith("mojang.com"))
{
return;
}
QSslCertificate cert = reply->sslConfiguration().peerCertificate();

QString serverHash = QCryptographicHash::hash(cert.publicKey().toDer(),QCryptographicHash::Sha256).toBase64();

if (URLConstants::AUTH_HASH.compare(serverHash) != 0)
{
qDebug()<< "Public Key Hashes don't match, abort";
m_currentTask->abortByCertPinning();
}
}


void MojangAccount::fillSession(AuthSessionPtr session)
{
// the user name. you have to have an user name
@@ -21,6 +21,7 @@
#include <QJsonObject>
#include <QPair>
#include <QMap>
#include <QNetworkAccessManager>

#include <memory>
#include "AuthSession.h"
@@ -72,7 +73,7 @@ class MULTIMC_LOGIC_EXPORT MojangAccount :
explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete;

//! Default constructor
explicit MojangAccount(QObject *parent = 0) : QObject(parent) {};
explicit MojangAccount(QObject *parent = 0);

//! Creates an empty account for the specified user name.
static MojangAccountPtr createFromUsername(const QString &username);
@@ -139,6 +140,8 @@ class MULTIMC_LOGIC_EXPORT MojangAccount :
// TODO: better signalling for the various possible state changes - especially errors

protected: /* variables */
QNetworkAccessManager m_auth_mgr;

QString m_username;

// Used to identify the client - the user can have multiple clients for the same account
@@ -170,6 +173,7 @@ private
slots:
void authSucceeded();
void authFailed(QString reason);
void certPinningHandler(QNetworkReply * reply);

private:
void fillSession(AuthSessionPtr session);
@@ -47,7 +47,7 @@ void YggdrasilTask::executeTask()
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");

QByteArray requestData = doc.toJson();
m_netReply = ENV.qnam().post(netRequest, requestData);
m_netReply = m_account->m_auth_mgr.post(netRequest, requestData);
connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply);
connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers);
connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers);
@@ -82,6 +82,14 @@ bool YggdrasilTask::abort()
return true;
}

void YggdrasilTask::abortByCertPinning()
{
progress(timeout_max, timeout_max);
m_aborted = YggdrasilTask::BY_DETECTED_MITM_ATTACK;
m_netReply->abort();
}


void YggdrasilTask::abortByTimeout()
{
progress(timeout_max, timeout_max);
@@ -114,21 +122,48 @@ void YggdrasilTask::processReply()
changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out."));
return;
case QNetworkReply::OperationCanceledError:
changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
{
if(m_aborted == YggdrasilTask::BY_DETECTED_MITM_ATTACK)
{
changeState(
STATE_FAILED_SECURITY,
tr(
"<b>Connection to Mojang authentication server is unsafe!</b><br/>There might be a few causes for it:<br/>"
"<ul>"
"<li>You willingly compromised your system by installing something like MCLeaks. <a "
"href=\"https://www.microsoft.com/en-us/download/details.aspx?id=38918\">Just get rid of it.</a></li>"
"<li>Some device on your network is interfering with encrypted traffic. In that case, "
"you have bigger worries than Minecraft not starting.</li>"
"<li>Our fingerprint of the mojang.net certificate is outdated.</li>"
"</ul>"
"In any case, please tell us about it."
)
);
}
else
{
changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
}
return;
}
case QNetworkReply::SslHandshakeFailedError:
{
changeState(
STATE_FAILED_SOFT,
tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
"<ul>"
"<li>You use Windows XP and need to <a "
"href=\"https://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
"your root certificates</a></li>"
"<li>Some device on your network is interfering with SSL traffic. In that case, "
"you have bigger worries than Minecraft not starting.</li>"
"<li>Possibly something else. Check the MultiMC log file for details</li>"
"</ul>"));
STATE_FAILED_SECURITY,
tr(
"<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
"<ul>"
"<li>You use Windows XP and need to <a "
"href=\"https://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
"your root certificates</a></li>"
"<li>Some device on your network is interfering with SSL traffic. In that case, "
"you have bigger worries than Minecraft not starting.</li>"
"<li>Possibly something else. Check the MultiMC log file for details</li>"
"</ul>"
)
);
return;
}
// used for invalid credentials and similar errors. Fall through.
case QNetworkReply::ContentAccessDenied:
case QNetworkReply::ContentOperationNotPermittedError:
@@ -65,7 +65,8 @@ class YggdrasilTask : public Task
{
BY_NOTHING,
BY_USER,
BY_TIMEOUT
BY_TIMEOUT,
BY_DETECTED_MITM_ATTACK
} m_aborted = BY_NOTHING;

/**
@@ -79,6 +80,7 @@ class YggdrasilTask : public Task
STATE_PROCESSING_RESPONSE,
STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated
STATE_FAILED_HARD, //!< hard failure. auth is invalid
STATE_FAILED_SECURITY, //!< hard failure. auth is invalid
STATE_SUCCEEDED
} m_state = STATE_CREATED;

@@ -134,6 +136,7 @@ public
slots:
virtual bool abort() override;
void abortByTimeout();
void abortByCertPinning();
State state();
protected:
// FIXME: segfault disaster waiting to happen
@@ -24,6 +24,7 @@ const QString RESOURCE_BASE("https://resources.download.minecraft.net/");
const QString LIBRARY_BASE("https://libraries.minecraft.net/");
const QString SKINS_BASE("https://crafatar.com/skins/");
const QString AUTH_BASE("https://authserver.mojang.com/");
const QString AUTH_HASH("Rxq9KKqlJ23qrz0GH/WTZhrZ5a2Ujq1A8PyrMrtjzig=");
const QString MOJANG_STATUS_URL("https://status.mojang.com/check");
const QString IMGUR_BASE_URL("https://api.imgur.com/3/");
const QString FMLLIBS_OUR_BASE_URL("https://files.multimc.org/fmllibs/");
@@ -97,7 +97,7 @@ void LaunchController::login()
{
// We'll need to validate the access token to make sure the account
// is still logged in.
ProgressDialog progDialog(m_parentWidget);
LoginDialog progDialog(m_parentWidget);
if (m_online)
{
progDialog.setSkipButton(true, tr("Play Offline"));
@@ -176,6 +176,26 @@ void LaunchController::login()
m_session->MakeOffline(usedname);
// offline flavored game from here :3
}
case AuthSession::SecurityError:
{
// we ask the user for a player name
bool ok = false;
QString usedname = m_session->player_name;
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"),
tr("Choose your offline mode player name."),
QLineEdit::Normal, m_session->player_name, &ok);
if (!ok)
{
tryagain = false;
break;
}
if (name.length())
{
usedname = name;
}
m_session->MakeOffline(usedname);
// offline flavored game from here :3
}
case AuthSession::PlayableOnline:
{
launchInstance();
@@ -45,8 +45,7 @@ void LoginDialog::accept()
m_account = MojangAccount::createFromUsername(ui->userTextBox->text());
m_loginTask = m_account->login(nullptr, ui->passTextBox->text());
connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this,
&LoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress);
m_loginTask->start();
@@ -62,13 +61,11 @@ void LoginDialog::setUserInputsEnabled(bool enable)
// Enable the OK button only when both textboxes contain something.
void LoginDialog::on_userTextBox_textEdited(const QString &newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty());
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty());
}
void LoginDialog::on_passTextBox_textEdited(const QString &newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty());
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty());
}

void LoginDialog::onTaskFailed(const QString &reason)

0 comments on commit d3ee724

Please sign in to comment.
You can’t perform that action at this time.