Reorganize source and add libraries

This commit is contained in:
Cameron Gutman 2018-05-05 17:25:55 -07:00
commit 63b6da601a
104 changed files with 39023 additions and 52 deletions

69
app/app.pro Normal file
View file

@ -0,0 +1,69 @@
#-------------------------------------------------
#
# Project created by QtCreator 2018-04-28T14:01:01
#
#-------------------------------------------------
QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = moonlight-qt
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
macx {
INCLUDEPATH += $$PWD/../libs/mac/include
LIBS += $$PWD/../libs/mac/lib/libssl.dylib
LIBS += $$PWD/../libs/mac/lib/libcrypto.dylib
}
unix:!macx {
CONFIG += link_pkgconfig
PKGCONFIG += openssl
}
SOURCES += \
main.cpp \
mainwindow.cpp \
nvhttp.cpp \
nvpairingmanager.cpp \
identitymanager.cpp \
popupmanager.cpp
HEADERS += \
mainwindow.h \
nvhttp.h \
nvpairingmanager.h \
identitymanager.h \
utils.h \
popupmanager.h
FORMS += \
mainwindow.ui
RESOURCES += \
resources.qrc
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../moonlight-common-c/release/ -lmoonlight-common-c
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../moonlight-common-c/debug/ -lmoonlight-common-c
else:unix: LIBS += -L$$OUT_PWD/../moonlight-common-c/ -lmoonlight-common-c
INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
DEPENDPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../opus/release/ -lopus
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../opus/debug/ -lopus
else:unix: LIBS += -L$$OUT_PWD/../opus/ -lopus
INCLUDEPATH += $$PWD/../opus/opus/include
DEPENDPATH += $$PWD/../opus/opus/include

143
app/identitymanager.cpp Normal file
View file

@ -0,0 +1,143 @@
#include "identitymanager.h"
#include "utils.h"
#include <QDebug>
#include <QFile>
#include <QTextStream>
#include <QSslCertificate>
#include <QSslKey>
#include <QStandardPaths>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/pkcs12.h>
IdentityManager::IdentityManager()
{
m_RootDirectory = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
if (!m_RootDirectory.exists())
{
m_RootDirectory.mkpath(".");
}
QFile uniqueIdFile(m_RootDirectory.filePath("uniqueid"));
if (uniqueIdFile.open(QIODevice::ReadOnly))
{
m_CachedUniqueId = QTextStream(&uniqueIdFile).readAll();
qDebug() << "Loaded cached unique ID: " << m_CachedUniqueId;
}
else
{
for (int i = 0; i < 16; i++)
{
int n = qrand() % 16;
m_CachedUniqueId.append(QString::number(n, 16));
}
qDebug() << "Generated new unique ID: " << m_CachedUniqueId;
uniqueIdFile.open(QIODevice::ReadWrite);
QTextStream(&uniqueIdFile) << m_CachedUniqueId;
}
QFile certificateFile(m_RootDirectory.filePath("cert"));
QFile privateKeyFile(m_RootDirectory.filePath("key"));
if (certificateFile.open(QIODevice::ReadOnly) && privateKeyFile.open(QIODevice::ReadOnly))
{
// Not cached yet, but it's on disk
m_CachedPemCert = certificateFile.readAll();
m_CachedPrivateKey = privateKeyFile.readAll();
// Make sure it really loads
if (!QSslKey(m_CachedPrivateKey, QSsl::Rsa).isNull() && !QSslCertificate(m_CachedPemCert).isNull())
{
qDebug() << "Loaded cached identity key pair from disk";
return;
}
qDebug() << "Regenerating corrupted local key pair";
}
privateKeyFile.close();
certificateFile.close();
X509* cert = X509_new();
THROW_BAD_ALLOC_IF_NULL(cert);
EVP_PKEY* pk = EVP_PKEY_new();
THROW_BAD_ALLOC_IF_NULL(pk);
BIGNUM* bne = BN_new();
THROW_BAD_ALLOC_IF_NULL(bne);
RSA* rsa = RSA_new();
THROW_BAD_ALLOC_IF_NULL(rsa);
BN_set_word(bne, RSA_F4);
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
EVP_PKEY_assign_RSA(pk, rsa);
X509_set_version(cert, 2);
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
X509_gmtime_adj(X509_get_notBefore(cert), 0);
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
X509_set_pubkey(cert, pk);
X509_NAME* name = X509_get_subject_name(cert);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast<unsigned char *>(const_cast<char*>("NVIDIA GameStream Client")), -1, -1, 0);
X509_set_issuer_name(cert, name);
X509_sign(cert, pk, EVP_sha1());
BIO* biokey = BIO_new(BIO_s_mem());
THROW_BAD_ALLOC_IF_NULL(biokey);
PEM_write_bio_PrivateKey(biokey, pk, NULL, NULL, 0, NULL, NULL);
BIO* biocert = BIO_new(BIO_s_mem());
THROW_BAD_ALLOC_IF_NULL(biocert);
PEM_write_bio_X509(biocert, cert);
privateKeyFile.open(QIODevice::WriteOnly);
certificateFile.open(QIODevice::WriteOnly);
BUF_MEM* mem;
BIO_get_mem_ptr(biokey, &mem);
m_CachedPrivateKey = QByteArray(mem->data, mem->length);
QTextStream(&privateKeyFile) << m_CachedPrivateKey;
BIO_get_mem_ptr(biocert, &mem);
m_CachedPemCert = QByteArray(mem->data, mem->length);
QTextStream(&certificateFile) << m_CachedPemCert;
X509_free(cert);
EVP_PKEY_free(pk);
BN_free(bne);
BIO_free(biokey);
BIO_free(biocert);
// Ensure we can actually consume the keys we just wrote
Q_ASSERT(!QSslCertificate(m_CachedPemCert).isNull());
Q_ASSERT(!QSslKey(m_CachedPrivateKey, QSsl::Rsa).isNull());
qDebug() << "Wrote new identity credentials to disk";
}
QString
IdentityManager::getUniqueId()
{
return m_CachedUniqueId;
}
QByteArray
IdentityManager::getCertificate()
{
return m_CachedPemCert;
}
QByteArray
IdentityManager::getPrivateKey()
{
return m_CachedPrivateKey;
}

25
app/identitymanager.h Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <QDir>
class IdentityManager
{
public:
IdentityManager();
QString
getUniqueId();
QByteArray
getCertificate();
QByteArray
getPrivateKey();
private:
QDir m_RootDirectory;
QByteArray m_CachedPrivateKey;
QByteArray m_CachedPemCert;
QString m_CachedUniqueId;
};

16
app/main.cpp Normal file
View file

@ -0,0 +1,16 @@
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
// MacOS directive to prevent the menu bar from being merged into the native bar
// i.e. it's in the window, and not the top left of the screen
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}

78
app/mainwindow.cpp Normal file
View file

@ -0,0 +1,78 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "popupmanager.h"
#include "identitymanager.h"
#include "nvpairingmanager.h"
#include "nvhttp.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
// sample code for an iconized button performing an action
// will be useful to implement the game grid UI later
// myButton = new QPushButton(this);
// myButton->setIcon(QIcon(":/res/icon128.png"));
// myButton->setIconSize(QSize(128, 128));
// myButton->resize(QSize(128, 128));
// connect(myButton, &QAbstractButton::clicked, this, &MainWindow::on_actionExit_triggered);
}
void MainWindow::closeEvent(QCloseEvent *event)
{
const QMessageBox::StandardButton ret
= QMessageBox::warning(this, tr("Application"),
tr("something-something-close?"),
QMessageBox::Yes | QMessageBox::No);
switch (ret) {
case QMessageBox::Yes:
event->accept();
break;
case QMessageBox::No:
default:
event->ignore();
break;
}
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_actionExit_triggered()
{
exit(EXIT_SUCCESS);
}
void MainWindow::on_newHostBtn_clicked()
{
QString hostname = popupmanager::getHostnameDialog(this);
if (!hostname.isEmpty()) {
IdentityManager im = IdentityManager();
NvPairingManager pm(hostname, im);
QString pin = pm.generatePinString();
NvHTTP http(hostname, im);
pm.pair(http.getServerInfo(), pin);
}
}
void MainWindow::addHostToDisplay(QMap<QString, bool> hostMdnsMap) {
QMapIterator<QString, bool> i(hostMdnsMap);
while (i.hasNext()) {
i.next();
ui->hostSelectCombo->addItem(i.key());
// we can ignore the mdns for now, it's only useful for displaying unpairing options
}
}
void MainWindow::on_selectHostComboBox_activated(const QString &selectedHostname)
{
// TODO: get all the applications that "selectedHostname" has listed
// probably populate another combobox of applications for the time being
}

33
app/mainwindow.h Normal file
View file

@ -0,0 +1,33 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QtWidgets>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
void on_actionExit_triggered();
void on_newHostBtn_clicked();
void addHostToDisplay(QMap<QString, bool>);
void on_selectHostComboBox_activated(const QString &);
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

102
app/mainwindow.ui Normal file
View file

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>483</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralWidget">
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>50</x>
<y>30</y>
<width>306</width>
<height>191</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item row="0" column="0">
<widget class="QPushButton" name="newHostBtn">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="text">
<string>Add New Host</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/res/icon128.png</normaloff>:/res/icon128.png</iconset>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="hostSelectCombo"/>
</item>
</layout>
</widget>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionSettings"/>
<addaction name="actionGamepad_Mapping"/>
<addaction name="actionExit"/>
</widget>
<addaction name="menuFile"/>
</widget>
<action name="actionSettings">
<property name="text">
<string>Settings</string>
</property>
</action>
<action name="actionGamepad_Mapping">
<property name="text">
<string>Gamepad Mapping</string>
</property>
</action>
<action name="actionExit">
<property name="text">
<string>Exit</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>

261
app/nvhttp.cpp Normal file
View file

@ -0,0 +1,261 @@
#include "nvhttp.h"
#include <QDebug>
#include <QUuid>
#include <QtNetwork/QNetworkReply>
#include <QEventLoop>
#include <QTimer>
#include <QXmlStreamReader>
#include <QSslKey>
#define REQUEST_TIMEOUT_MS 5000
NvHTTP::NvHTTP(QString address, IdentityManager im) :
m_Address(address),
m_Im(im)
{
m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https");
m_BaseUrlHttp.setHost(address);
m_BaseUrlHttps.setHost(address);
m_BaseUrlHttp.setPort(47989);
m_BaseUrlHttps.setPort(47984);
}
NvComputer
NvHTTP::getComputerInfo()
{
NvComputer computer;
QString serverInfo = getServerInfo();
computer.m_Name = getXmlString(serverInfo, "hostname");
if (computer.m_Name == nullptr)
{
computer.m_Name = "UNKNOWN";
}
computer.m_Uuid = getXmlString(serverInfo, "uniqueid");
computer.m_MacAddress = getXmlString(serverInfo, "mac");
// If there's no LocalIP field, use the address we hit the server on
computer.m_LocalAddress = getXmlString(serverInfo, "LocalIP");
if (computer.m_LocalAddress == nullptr)
{
computer.m_LocalAddress = m_Address;
}
// If there's no ExternalIP field, use the address we hit the server on
computer.m_RemoteAddress = getXmlString(serverInfo, "ExternalIP");
if (computer.m_RemoteAddress == nullptr)
{
computer.m_RemoteAddress = m_Address;
}
computer.m_PairState = getXmlString(serverInfo, "PairStatus") == "1" ?
NvComputer::PS_PAIRED : NvComputer::PS_NOT_PAIRED;
computer.m_RunningGameId = getCurrentGame(serverInfo);
computer.m_State = NvComputer::CS_ONLINE;
return computer;
}
QVector<int>
NvHTTP::getServerVersionQuad(QString serverInfo)
{
QString quad = getXmlString(serverInfo, "appversion");
QStringList parts = quad.split(".");
QVector<int> ret;
for (int i = 0; i < 4; i++)
{
ret.append(parts.at(i).toInt());
}
return ret;
}
int
NvHTTP::getCurrentGame(QString serverInfo)
{
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
// has the semantics that its name would indicate. To contain the effects of this change as much
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
QString serverState = getXmlString(serverInfo, "state");
if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY"))
{
return getXmlString(serverInfo, "currentgame").toInt();
}
else
{
return 0;
}
}
QString
NvHTTP::getServerInfo()
{
QString serverInfo;
try
{
// Always try HTTPS first, since it properly reports
// pairing status (and a few other attributes).
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo",
nullptr,
true);
// Throws if the request failed
verifyResponseStatus(serverInfo);
}
catch (const GfeHttpResponseException& e)
{
if (e.getStatusCode() == 401)
{
// Certificate validation error, fallback to HTTP
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
true);
verifyResponseStatus(serverInfo);
}
else
{
// Rethrow real errors
throw e;
}
}
return serverInfo;
}
void
NvHTTP::verifyResponseStatus(QString xml)
{
QXmlStreamReader xmlReader(xml);
while (xmlReader.readNextStartElement())
{
if (xmlReader.name() == "root")
{
int statusCode = xmlReader.attributes().value("status_code").toInt();
if (statusCode == 200)
{
// Successful
return;
}
else
{
QString statusMessage = xmlReader.attributes().value("status_message").toString();
qDebug() << "Request failed: " << statusCode << " " << statusMessage;
throw GfeHttpResponseException(statusCode, statusMessage);
}
}
}
}
QByteArray
NvHTTP::getXmlStringFromHex(QString xml,
QString tagName)
{
QString str = getXmlString(xml, tagName);
if (str == nullptr)
{
return nullptr;
}
return QByteArray::fromHex(str.toLatin1());
}
QString
NvHTTP::getXmlString(QString xml,
QString tagName)
{
QXmlStreamReader xmlReader(xml);
while (!xmlReader.atEnd())
{
if (xmlReader.readNext() != QXmlStreamReader::StartElement)
{
continue;
}
if (xmlReader.name() == tagName)
{
return xmlReader.readElementText();
}
}
return nullptr;
}
QString
NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout)
{
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
QString ret;
ret = QTextStream(reply).readAll();
delete reply;
return ret;
}
QNetworkReply*
NvHTTP::openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout)
{
// Build a URL for the request
QUrl url(baseUrl);
url.setPath("/" + command);
url.setQuery("uniqueid=" + m_Im.getUniqueId() +
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
((arguments != nullptr) ? ("&" + arguments) : ""));
QNetworkRequest request = QNetworkRequest(url);
// Add our client certificate
QSslConfiguration sslConfig(QSslConfiguration::defaultConfiguration());
sslConfig.setLocalCertificate(QSslCertificate(m_Im.getCertificate()));
sslConfig.setPrivateKey(QSslKey(m_Im.getPrivateKey(), QSsl::Rsa));
request.setSslConfiguration(sslConfig);
QNetworkReply* reply = m_Nam.get(request);
// Ignore self-signed certificate errors (since GFE uses them)
reply->ignoreSslErrors();
// Run the request with a timeout if requested
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
if (enableTimeout)
{
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
}
qDebug() << "Executing request: " << url.toString();
loop.exec(QEventLoop::ExcludeUserInputEvents);
// Abort the request if it timed out
if (!reply->isFinished())
{
qDebug() << "Aborting timed out request for " << url.toString();
reply->abort();
}
// Handle error
if (reply->error() != QNetworkReply::NoError)
{
qDebug() << command << " request failed with error " << reply->error();
GfeHttpResponseException exception(reply->error(), reply->errorString());
delete reply;
throw exception;
}
return reply;
}

113
app/nvhttp.h Normal file
View file

@ -0,0 +1,113 @@
#pragma once
#include "identitymanager.h"
#include <QUrl>
#include <QtNetwork/QNetworkAccessManager>
class GfeHttpResponseException : public std::exception
{
public:
GfeHttpResponseException(int statusCode, QString message) :
m_StatusCode(statusCode),
m_StatusMessage(message)
{
}
const char* what() const throw()
{
return m_StatusMessage.toLatin1();
}
const char* getStatusMessage() const
{
return m_StatusMessage.toLatin1();
}
int getStatusCode() const
{
return m_StatusCode;
}
private:
int m_StatusCode;
QString m_StatusMessage;
};
class NvComputer
{
public:
NvComputer() {}
enum PairState
{
PS_UNKNOWN,
PS_PAIRED,
PS_NOT_PAIRED
};
enum ComputerState
{
CS_UNKNOWN,
CS_ONLINE,
CS_OFFLINE
};
QString m_Name;
QString m_Uuid;
QString m_MacAddress;
QString m_LocalAddress;
QString m_RemoteAddress;
int m_RunningGameId;
PairState m_PairState;
ComputerState m_State;
};
class NvHTTP
{
public:
NvHTTP(QString address, IdentityManager im);
NvComputer
getComputerInfo();
int
getCurrentGame(QString serverInfo);
QString
getServerInfo();
void
verifyResponseStatus(QString xml);
QString
getXmlString(QString xml,
QString tagName);
QByteArray
getXmlStringFromHex(QString xml,
QString tagName);
QString
openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout);
QVector<int>
getServerVersionQuad(QString serverInfo);
QUrl m_BaseUrlHttp;
QUrl m_BaseUrlHttps;
private:
QNetworkReply*
openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout);
QString m_Address;
QNetworkAccessManager m_Nam;
IdentityManager m_Im;
};

318
app/nvpairingmanager.cpp Normal file
View file

@ -0,0 +1,318 @@
#include "nvpairingmanager.h"
#include "utils.h"
#include <QRandomGenerator>
#include <openssl/bio.h>
#include <openssl/aes.h>
#include <openssl/rand.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/evp.h>
NvPairingManager::NvPairingManager(QString address, IdentityManager im) :
m_Http(address, im),
m_Im(im)
{
QByteArray cert = m_Im.getCertificate();
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
THROW_BAD_ALLOC_IF_NULL(bio);
m_Cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free_all(bio);
if (m_Cert == nullptr)
{
throw new std::runtime_error("Unable to load certificate");
}
QByteArray pk = m_Im.getPrivateKey();
bio = BIO_new_mem_buf(pk.data(), -1);
THROW_BAD_ALLOC_IF_NULL(bio);
m_PrivateKey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
BIO_free_all(bio);
if (m_PrivateKey == nullptr)
{
throw new std::runtime_error("Unable to load private key");
}
}
NvPairingManager::~NvPairingManager()
{
X509_free(m_Cert);
EVP_PKEY_free(m_PrivateKey);
}
QString
NvPairingManager::generatePinString()
{
return QString::asprintf("%04d", QRandomGenerator::global()->bounded(10000));
}
QByteArray
NvPairingManager::generateRandomBytes(int length)
{
char* data = static_cast<char*>(alloca(length));
RAND_bytes(reinterpret_cast<unsigned char*>(data), length);
return QByteArray(data, length);
}
QByteArray
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
{
QByteArray ciphertext(plaintext.size(), 0);
for (int i = 0; i < plaintext.size(); i += 16)
{
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
key);
}
return ciphertext;
}
QByteArray
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
{
QByteArray plaintext(ciphertext.size(), 0);
for (int i = 0; i < plaintext.size(); i += 16)
{
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
key);
}
return plaintext;
}
QByteArray
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
{
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
THROW_BAD_ALLOC_IF_NULL(bio);
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free_all(bio);
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
#else
const ASN1_BIT_STRING *asnSignature;
#endif
X509_get0_signature(&asnSignature, NULL, cert);
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
X509_free(cert);
return signature;
}
bool
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
{
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
THROW_BAD_ALLOC_IF_NULL(bio);
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free_all(bio);
EVP_PKEY* pubKey = X509_get_pubkey(cert);
THROW_BAD_ALLOC_IF_NULL(pubKey);
EVP_MD_CTX* mdctx = EVP_MD_CTX_create();
THROW_BAD_ALLOC_IF_NULL(mdctx);
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
EVP_PKEY_free(pubKey);
EVP_MD_CTX_destroy(mdctx);
X509_free(cert);
return result > 0;
}
QByteArray
NvPairingManager::signMessage(QByteArray message)
{
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
THROW_BAD_ALLOC_IF_NULL(ctx);
const EVP_MD *md = EVP_get_digestbyname("SHA256");
THROW_BAD_ALLOC_IF_NULL(md);
EVP_DigestInit_ex(ctx, md, NULL);
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
size_t signatureLength = 0;
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
QByteArray signature(signatureLength, 0);
EVP_DigestSignFinal(ctx, reinterpret_cast<unsigned char*>(signature.data()), &signatureLength);
EVP_MD_CTX_destroy(ctx);
return signature;
}
QByteArray
NvPairingManager::saltPin(QByteArray salt, QString pin)
{
return QByteArray().append(salt).append(pin.toLatin1());
}
NvPairingManager::PairState
NvPairingManager::pair(QString serverInfo, QString pin)
{
int serverMajorVersion = m_Http.getServerVersionQuad(serverInfo).at(0);
qDebug() << "Pairing with server generation: " << serverMajorVersion;
QCryptographicHash::Algorithm hashAlgo;
int hashLength;
if (serverMajorVersion >= 7)
{
// Gen 7+ uses SHA-256 hashing
hashAlgo = QCryptographicHash::Sha256;
hashLength = 32;
}
else
{
// Prior to Gen 7 uses SHA-1 hashing
hashAlgo = QCryptographicHash::Sha1;
hashLength = 20;
}
QByteArray salt = generateRandomBytes(16);
QByteArray saltedPin = saltPin(salt, pin);
AES_KEY encKey, decKey;
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
salt.toHex() + "&clientcert=" + m_Im.getCertificate().toHex(),
false);
m_Http.verifyResponseStatus(getCert);
if (m_Http.getXmlString(getCert, "paired") != "1")
{
qDebug() << "Failed pairing at stage #1";
return PairState::FAILED;
}
QByteArray serverCert = m_Http.getXmlStringFromHex(getCert, "plaincert");
if (serverCert == nullptr)
{
qDebug() << "Server likely already pairing";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::ALREADY_IN_PROGRESS;
}
QByteArray randomChallenge = generateRandomBytes(16);
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&clientchallenge=" +
encryptedChallenge.toHex(),
true);
m_Http.verifyResponseStatus(challengeXml);
if (m_Http.getXmlString(challengeXml, "paired") != "1")
{
qDebug() << "Failed pairing at stage #2";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
QByteArray clientSecretData = generateRandomBytes(16);
QByteArray challengeResponse;
QByteArray serverResponse(challengeResponseData.data(), hashLength);
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
#else
const ASN1_BIT_STRING *asnSignature;
#endif
X509_get0_signature(&asnSignature, NULL, m_Cert);
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
challengeResponse.append(clientSecretData);
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&serverchallengeresp=" +
encryptedChallengeResponseHash.toHex(),
true);
m_Http.verifyResponseStatus(respXml);
if (m_Http.getXmlString(respXml, "paired") != "1")
{
qDebug() << "Failed pairing at stage #3";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
QByteArray pairingSecret = m_Http.getXmlStringFromHex(respXml, "pairingsecret");
QByteArray serverSecret = QByteArray(pairingSecret.data(), 16);
QByteArray serverSignature = QByteArray(&pairingSecret.data()[16], 256);
if (!verifySignature(serverSecret,
serverSignature,
serverCert))
{
qDebug() << "MITM detected";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
QByteArray expectedResponseData;
expectedResponseData.append(randomChallenge);
expectedResponseData.append(getSignatureFromPemCert(serverCert));
expectedResponseData.append(serverSecret);
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
{
qDebug() << "Incorrect PIN";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::PIN_WRONG;
}
QByteArray clientPairingSecret;
clientPairingSecret.append(clientSecretData);
clientPairingSecret.append(signMessage(clientSecretData));
QString secretRespXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&clientpairingsecret=" +
clientPairingSecret.toHex(),
true);
m_Http.verifyResponseStatus(secretRespXml);
if (m_Http.getXmlString(secretRespXml, "paired") != "1")
{
qDebug() << "Failed pairing at stage #4";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
"pair",
"devicename=roth&updateState=1&phase=pairchallenge",
true);
m_Http.verifyResponseStatus(pairChallengeXml);
if (m_Http.getXmlString(pairChallengeXml, "paired") != "1")
{
qDebug() << "Failed pairing at stage #5";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
return PairState::FAILED;
}
return PairState::PAIRED;
}

58
app/nvpairingmanager.h Normal file
View file

@ -0,0 +1,58 @@
#pragma once
#include <identitymanager.h>
#include <nvhttp.h>
#include <openssl/aes.h>
#include <openssl/x509.h>
#include <openssl/evp.h>
class NvPairingManager
{
public:
enum PairState
{
NOT_PAIRED,
PAIRED,
PIN_WRONG,
FAILED,
ALREADY_IN_PROGRESS
};
NvPairingManager(QString address, IdentityManager im);
~NvPairingManager();
QString
generatePinString();
PairState
pair(QString serverInfo, QString pin);
private:
QByteArray
generateRandomBytes(int length);
QByteArray
saltPin(QByteArray salt, QString pin);
QByteArray
encrypt(QByteArray plaintext, AES_KEY* key);
QByteArray
decrypt(QByteArray ciphertext, AES_KEY* key);
QByteArray
getSignatureFromPemCert(QByteArray certificate);
bool
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
QByteArray
signMessage(QByteArray message);
NvHTTP m_Http;
IdentityManager m_Im;
X509* m_Cert;
EVP_PKEY* m_PrivateKey;
};

41
app/popupmanager.cpp Normal file
View file

@ -0,0 +1,41 @@
#include "popupmanager.h"
QMessageBox *popupmanager::pinMsgBox = nullptr;
popupmanager::popupmanager(){}
// this opens a non-blocking informative message telling the user to enter the given pin
// it is open-loop: if the user cancels, nothing happens
// it is expected that upon pairing completion, the ::closePinDialog function will be called.
void popupmanager::displayPinDialog(QString pin, QWidget* parent) {
popupmanager::pinMsgBox = new QMessageBox( parent );
popupmanager::pinMsgBox->setAttribute( Qt::WA_DeleteOnClose ); //makes sure the msgbox is deleted automatically when closed
popupmanager::pinMsgBox->setStandardButtons( QMessageBox::Ok );
popupmanager::pinMsgBox->setText("Please enter the number " + pin + " on the GFE dialog on the computer.");
popupmanager::pinMsgBox->setInformativeText("This dialog will be dismissed once complete.");
popupmanager::pinMsgBox->open();
}
// to be called when the pairing is complete
void popupmanager::closePinDialog() {
pinMsgBox->close();
delete pinMsgBox;
}
QString popupmanager::getHostnameDialog(QWidget* parent) {
bool ok;
QString responseHost
= QInputDialog::getText(parent, QObject::tr("Add Host Manually"),
QObject::tr("IP Address or Hostname of GeForce PC"),
QLineEdit::Normal,
QObject::tr("default string"),
&ok);
if (ok && !responseHost.isEmpty()) {
return responseHost;
} else {
return QObject::tr("");
}
}

18
app/popupmanager.h Normal file
View file

@ -0,0 +1,18 @@
#ifndef POPUPMANAGER_H
#define POPUPMANAGER_H
#include <QtWidgets>
class popupmanager
{
public:
popupmanager();
static void displayPinDialog(QString pin, QWidget* parent);
static void closePinDialog();
static QString getHostnameDialog(QWidget* parent);
private:
static QMessageBox *pinMsgBox;
};
#endif // POPUPMANAGER_H

View file

@ -0,0 +1,4 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-7v2h-3v3h-2v-3H8v-2h3V7h2v3h3z"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View file

@ -0,0 +1,4 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 303 B

View file

@ -0,0 +1,9 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M0 0h24v24H0V0z" id="a"/>
</defs>
<clipPath id="b">
<use overflow="visible" xlink:href="#a"/>
</clipPath>
<path clip-path="url(#b)" d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-7v2H8v-2h8z"/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

View file

@ -0,0 +1,4 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z"/>
</svg>

After

Width:  |  Height:  |  Size: 273 B

View file

@ -0,0 +1,4 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0v24h24V0H0zm23 16c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V8c0-1.1.9-2 2-2h18c1.1 0 2 .9 2 2v8z" fill="none"/>
<path d="M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-10 7H8v3H6v-3H3v-2h3V8h2v3h3v2zm4.5 2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4-3c-.83 0-1.5-.67-1.5-1.5S18.67 9 19.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 501 B

BIN
app/res/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/res/no_app_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

11
app/resources.qrc Normal file
View file

@ -0,0 +1,11 @@
<RCC>
<qresource prefix="/">
<file>res/icon128.png</file>
<file>res/ic_add_to_queue_white_48px.svg</file>
<file>res/ic_remove_circle_outline_white_48px.svg</file>
<file>res/ic_remove_from_queue_white_48px.svg</file>
<file>res/ic_tv_white_48px.svg</file>
<file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/no_app_image.png</file>
</qresource>
</RCC>

4
app/utils.h Normal file
View file

@ -0,0 +1,4 @@
#pragma once
#define THROW_BAD_ALLOC_IF_NULL(x) \
if ((x) == nullptr) throw new std::bad_alloc()