diff --git a/identitymanager.cpp b/identitymanager.cpp new file mode 100644 index 00000000..731c4805 --- /dev/null +++ b/identitymanager.cpp @@ -0,0 +1,113 @@ +#include "identitymanager.h" + +#include + +#include +#include +#include + +IdentityManager::IdentityManager(QDir directory) + : m_RootDirectory(directory), + m_CachedPrivateKey(nullptr), + m_CachedPemCert(nullptr), + m_CachedUniqueId(nullptr) +{ +} + +QString +IdentityManager::getUniqueId() +{ + if (m_CachedUniqueId == nullptr) + { + QFile uniqueIdFile(m_RootDirectory.filePath("uniqueid")); + if (uniqueIdFile.open(QIODevice::ReadOnly)) + { + m_CachedUniqueId = QTextStream(uniqueIdFile.open(QIODevice::ReadOnly)).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; + } + } + + return m_CachedUniqueId; +} + +void +IdentityManager::loadKeyPair() +{ + if (m_CachedPemCert != nullptr && m_CachedPrivateKey != nullptr) + { + // Already have cached data + return; + } + + 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(); + + qDebug() << "Loaded cached identity credentials from disk"; + return; + } + + X509* cert = X509_new(); + if (cert == nullptr) + { + throw new std::bad_alloc(); + } + + EVP_PKEY* pk = EVP_PKEY_new(); + if (pk == nullptr) + { + X509_free(cert); + throw new std::bad_alloc(); + } + + EVP_PKEY_assign_RSA(pk, RSA_generate_key(2048, RSA_F4, nullptr, nullptr)); + + X509_set_version(cert, 2); + X509_gmtime_adj(X509_get_notBefore(cert), 0); + X509_gmtime_adj(X509_get0_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("NVIDIA GameStream Client"), -1, -1, 0); + X509_set_issuer_name(x, name); + + privateKeyFile.open(QIODevice::ReadWrite); + PEM_write_PrivateKey(privateKeyFile.handle(), pk, nullptr, nullptr, 0, nullptr, nullptr); + + certificateFile.open(QIODevice::ReadWrite); + PEM_write_X509(certificateFile.handle(), cert); + + qDebug() << "Wrote new identity credentials to disk"; +} + +QByteArray +IdentityManager::getCertificate() +{ + loadKeyPair(); + return m_CachedPemCert; +} + +QByteArray +IdentityManager::getPrivateKey() +{ + loadKeyPair(); + return m_CachedPrivateKey; +} diff --git a/identitymanager.h b/identitymanager.h new file mode 100644 index 00000000..acd5bc03 --- /dev/null +++ b/identitymanager.h @@ -0,0 +1,15 @@ +#pragma once + + +class IdentityManager +{ +public: + IdentityManager(QDir directory); + +private: + QDir m_RootDirectory; + + QString m_CachedUniqueId; + QByteArray m_CachedPemCert; + QByteArray m_CachedPrivateKey; +}; diff --git a/moonlight-qt.pro b/moonlight-qt.pro index ca0080e2..2971d046 100644 --- a/moonlight-qt.pro +++ b/moonlight-qt.pro @@ -20,17 +20,25 @@ 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 +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ - main.cpp \ - mainwindow.cpp \ - nvhttp.cpp + main.cpp \ + mainwindow.cpp \ + nvhttp.cpp \ + nvpairingmanager.cpp \ + identitymanager.cpp HEADERS += \ - mainwindow.h \ - nvhttp.h + mainwindow.h \ + nvhttp.h \ + nvpairingmanager.h \ + identitymanager.h FORMS += \ - mainwindow.ui + mainwindow.ui + +OPENSSL_LIBS='-L/usr/lib -lssl -lcrypto' + +QMAKE_CXXFLAGS += -openssl-linked diff --git a/nvhttp.cpp b/nvhttp.cpp index 8caf423a..b91d4992 100644 --- a/nvhttp.cpp +++ b/nvhttp.cpp @@ -58,6 +58,19 @@ NvHTTP::getComputerInfo() return computer; } +QVector +NvHTTP::getServerVersionQuad(QString serverInfo) +{ + QString quad = getXmlString(serverInfo, "appversion"); + QStringList parts = quad.split("."); + QVector ret; + + for (int i = 0; i < 4; i++) + { + ret.append(parts.at(i).toInt()); + } +} + int NvHTTP::getCurrentGame(QString serverInfo) { @@ -65,10 +78,12 @@ NvHTTP::getCurrentGame(QString serverInfo) // 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")) { + if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY")) + { return getXmlString(serverInfo, "currentgame").toInt(); } - else { + else + { return 0; } } @@ -201,8 +216,9 @@ NvHTTP::openConnection(QUrl baseUrl, if (reply->error() != QNetworkReply::NoError) { qDebug() << command << " request for failed with error " << reply->error(); + GfeHttpResponseException* exception = new GfeHttpResponseException(reply->error(), reply->errorString()); delete reply; - throw new GfeHttpResponseException(reply->error(), reply->errorString()); + throw exception; } return reply; diff --git a/nvhttp.h b/nvhttp.h index f0a3fe5f..fd981088 100644 --- a/nvhttp.h +++ b/nvhttp.h @@ -96,6 +96,9 @@ private: QString arguments, bool enableTimeout); + QVector + getServerVersionQuad(QString serverInfo); + QString m_Address; QUrl m_BaseUrlHttp; QUrl m_BaseUrlHttps; diff --git a/nvpairingmanager.cpp b/nvpairingmanager.cpp new file mode 100644 index 00000000..9372b68f --- /dev/null +++ b/nvpairingmanager.cpp @@ -0,0 +1,67 @@ +#include "nvpairingmanager.h" + +#include + +#include + +NvPairingManager::NvPairingManager(QString address) : + m_Http(address) +{ + +} + +QString +NvPairingManager::generatePinString() +{ + return QString::number(QRandomGenerator::global()->bounded(10000)); +} + +QByteArray +NvPairingManager::generateRandomBytes(int length) +{ + QByteArray array(length); + + for (int i = 0; i < length; i++) + { + array.data()[i] = static_cast(QRandomGenerator::global()->bounded(256)); + } + + return array; +} + +QByteArray +NvPairingManager::saltPin(QByteArray salt, QString pin) +{ + return QByteArray().append(salt).append(pin.toStdString().c_str()); +} + +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; + if (serverMajorVersion >= 7) + { + // Gen 7+ uses SHA-256 hashing + hashAlgo = QCryptographicHash::Sha256; + } + else + { + // Prior to Gen 7 uses SHA-1 hashing + hashAlgo = QCryptographicHash::Sha1; + } + + QByteArray salt = generateRandomBytes(16); + QByteArray saltedPin = saltPin(salt, pin); + + AES_KEY encKey, decKey; + AES_set_decrypt_key(QCryptographicHash::hash(saltedPin, hashAlgo).data(), 128, &decKey); + AES_set_encrypt_key(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=" + ) +} diff --git a/nvpairingmanager.h b/nvpairingmanager.h new file mode 100644 index 00000000..d5ac80ed --- /dev/null +++ b/nvpairingmanager.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class NvPairingManager +{ +public: + enum PairState + { + NOT_PAIRED, + PAIRED, + PIN_WRONG, + FAILED, + ALREADY_IN_PROGRESS + }; + + NvPairingManager(QString address); + + QString + generatePinString(); + + PairState + pair(QString serverInfo, QString pin); + +private: + NvHTTP m_Http; +};