From a5d50cd4f0525ef62769eb0d0f0ccf461fb5bed1 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 28 Apr 2018 22:14:27 -0700 Subject: [PATCH] more work --- identitymanager.cpp | 103 ++++++++++------------ identitymanager.h | 14 ++- moonlight-qt.pro | 9 +- nvhttp.cpp | 26 ++++-- nvhttp.h | 29 +++--- nvpairingmanager.cpp | 204 ++++++++++++++++++++++++++++++++++++++++--- nvpairingmanager.h | 30 ++++++- utils.h | 4 + 8 files changed, 322 insertions(+), 97 deletions(-) create mode 100644 utils.h diff --git a/identitymanager.cpp b/identitymanager.cpp index 731c4805..1779fb8a 100644 --- a/identitymanager.cpp +++ b/identitymanager.cpp @@ -1,55 +1,34 @@ #include "identitymanager.h" +#include "utils.h" +#include #include +#include #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(directory.filePath("uniqueid")); + if (uniqueIdFile.open(QIODevice::ReadOnly)) { - 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; - } + m_CachedUniqueId = QTextStream(&uniqueIdFile).readAll(); + qDebug() << "Loaded cached unique ID: " << m_CachedUniqueId; } - - return m_CachedUniqueId; -} - -void -IdentityManager::loadKeyPair() -{ - if (m_CachedPemCert != nullptr && m_CachedPrivateKey != nullptr) + else { - // Already have cached data - return; + 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")); @@ -66,48 +45,58 @@ IdentityManager::loadKeyPair() } X509* cert = X509_new(); - if (cert == nullptr) - { - throw new std::bad_alloc(); - } + THROW_BAD_ALLOC_IF_NULL(cert); EVP_PKEY* pk = EVP_PKEY_new(); - if (pk == nullptr) - { - X509_free(cert); - throw new std::bad_alloc(); - } + THROW_BAD_ALLOC_IF_NULL(pk); - EVP_PKEY_assign_RSA(pk, RSA_generate_key(2048, RSA_F4, nullptr, nullptr)); + 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); X509_gmtime_adj(X509_get_notBefore(cert), 0); - X509_gmtime_adj(X509_get0_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs + 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("NVIDIA GameStream Client"), -1, -1, 0); - X509_set_issuer_name(x, name); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast(const_cast("NVIDIA GameStream Client")), -1, -1, 0); + X509_set_issuer_name(cert, name); privateKeyFile.open(QIODevice::ReadWrite); - PEM_write_PrivateKey(privateKeyFile.handle(), pk, nullptr, nullptr, 0, nullptr, nullptr); + PEM_write_PrivateKey(fdopen(privateKeyFile.handle(), "w"), pk, nullptr, nullptr, 0, nullptr, nullptr); certificateFile.open(QIODevice::ReadWrite); - PEM_write_X509(certificateFile.handle(), cert); + PEM_write_X509(fdopen(certificateFile.handle(), "w"), cert); + + X509_free(cert); + EVP_PKEY_free(pk); + BN_free(bne); qDebug() << "Wrote new identity credentials to disk"; } +QString +IdentityManager::getUniqueId() +{ + return m_CachedUniqueId; +} + QByteArray IdentityManager::getCertificate() { - loadKeyPair(); return m_CachedPemCert; } QByteArray IdentityManager::getPrivateKey() { - loadKeyPair(); return m_CachedPrivateKey; } diff --git a/identitymanager.h b/identitymanager.h index acd5bc03..bda49413 100644 --- a/identitymanager.h +++ b/identitymanager.h @@ -1,15 +1,25 @@ #pragma once +#include class IdentityManager { public: IdentityManager(QDir directory); + QString + getUniqueId(); + + QByteArray + getCertificate(); + + QByteArray + getPrivateKey(); + private: QDir m_RootDirectory; - QString m_CachedUniqueId; - QByteArray m_CachedPemCert; QByteArray m_CachedPrivateKey; + QByteArray m_CachedPemCert; + QString m_CachedUniqueId; }; diff --git a/moonlight-qt.pro b/moonlight-qt.pro index 2971d046..c730c792 100644 --- a/moonlight-qt.pro +++ b/moonlight-qt.pro @@ -34,11 +34,12 @@ HEADERS += \ mainwindow.h \ nvhttp.h \ nvpairingmanager.h \ - identitymanager.h + identitymanager.h \ + utils.h FORMS += \ mainwindow.ui -OPENSSL_LIBS='-L/usr/lib -lssl -lcrypto' - -QMAKE_CXXFLAGS += -openssl-linked +LIBS += \ + -lssl \ + -lcrypto diff --git a/nvhttp.cpp b/nvhttp.cpp index b91d4992..025fd306 100644 --- a/nvhttp.cpp +++ b/nvhttp.cpp @@ -8,8 +8,9 @@ #define REQUEST_TIMEOUT_MS 5000 -NvHTTP::NvHTTP(QString address) : - m_Address(address) +NvHTTP::NvHTTP(QString address, IdentityManager im) : + m_Address(address), + m_Im(im) { m_BaseUrlHttp.setScheme("http"); m_BaseUrlHttps.setScheme("https"); @@ -69,6 +70,8 @@ NvHTTP::getServerVersionQuad(QString serverInfo) { ret.append(parts.at(i).toInt()); } + + return ret; } int @@ -104,7 +107,7 @@ NvHTTP::getServerInfo() // Throws if the request failed verifyResponseStatus(serverInfo); } - catch (GfeHttpResponseException& e) + catch (const GfeHttpResponseException& e) { if (e.getStatusCode() == 401) { @@ -115,6 +118,11 @@ NvHTTP::getServerInfo() true); verifyResponseStatus(serverInfo); } + else + { + // Rethrow real errors + throw e; + } } return serverInfo; @@ -185,10 +193,10 @@ NvHTTP::openConnection(QUrl baseUrl, { // Build a URL for the request QUrl url(baseUrl); - url.setPath(command + - "?uniqueid=" + "0" + - "&uuid=" + QUuid::createUuid().toString() + - ((arguments != nullptr) ? (arguments + "&") : "")); + url.setPath("/" + command); + url.setQuery("uniqueid=" + m_Im.getUniqueId() + + "&uuid=" + QUuid::createUuid().toRfc4122().toHex() + + ((arguments != nullptr) ? ("&" + arguments) : "")); QNetworkReply* reply = m_Nam.get(QNetworkRequest(url)); @@ -215,8 +223,8 @@ NvHTTP::openConnection(QUrl baseUrl, // Handle error if (reply->error() != QNetworkReply::NoError) { - qDebug() << command << " request for failed with error " << reply->error(); - GfeHttpResponseException* exception = new GfeHttpResponseException(reply->error(), reply->errorString()); + qDebug() << command << " request failed with error " << reply->error(); + GfeHttpResponseException exception(reply->error(), reply->errorString()); delete reply; throw exception; } diff --git a/nvhttp.h b/nvhttp.h index fd981088..57aa24db 100644 --- a/nvhttp.h +++ b/nvhttp.h @@ -1,5 +1,7 @@ #pragma once +#include "identitymanager.h" + #include #include @@ -15,15 +17,15 @@ public: const char* what() const throw() { - return m_StatusMessage.toStdString().c_str(); + return m_StatusMessage.toLatin1(); } - const char* getStatusMessage() + const char* getStatusMessage() const { - return m_StatusMessage.toStdString().c_str(); + return m_StatusMessage.toLatin1(); } - int getStatusCode() + int getStatusCode() const { return m_StatusCode; } @@ -65,14 +67,7 @@ public: class NvHTTP { public: - NvHTTP(QString address); - -private: - QNetworkReply* - openConnection(QUrl baseUrl, - QString command, - QString arguments, - bool enableTimeout); + NvHTTP(QString address, IdentityManager im); NvComputer getComputerInfo(); @@ -99,8 +94,16 @@ private: QVector getServerVersionQuad(QString serverInfo); - QString m_Address; 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; }; diff --git a/nvpairingmanager.cpp b/nvpairingmanager.cpp index 9372b68f..14b40072 100644 --- a/nvpairingmanager.cpp +++ b/nvpairingmanager.cpp @@ -1,13 +1,44 @@ #include "nvpairingmanager.h" +#include "utils.h" #include +#include #include +#include +#include +#include +#include -NvPairingManager::NvPairingManager(QString address) : - m_Http(address) +NvPairingManager::NvPairingManager(QString address, IdentityManager im) : + m_Http(address, im), + m_Im(im) { + BIO *bio = BIO_new_mem_buf(m_Im.getCertificate().toStdString().c_str(), -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"); + } + + bio = BIO_new_mem_buf(m_Im.getPrivateKey().toStdString().c_str(), -1); + THROW_BAD_ALLOC_IF_NULL(bio); + + PEM_read_bio_PrivateKey(bio, &m_PrivateKey, nullptr, nullptr); + BIO_free_all(bio); + if (m_Cert == nullptr) + { + throw new std::runtime_error("Unable to load private key"); + } +} + +NvPairingManager::~NvPairingManager() +{ + X509_free(m_Cert); + EVP_PKEY_free(m_PrivateKey); } QString @@ -19,20 +50,88 @@ NvPairingManager::generatePinString() QByteArray NvPairingManager::generateRandomBytes(int length) { - QByteArray array(length); + char* data = static_cast(alloca(length)); + RAND_bytes(reinterpret_cast(data), length); + return QByteArray(data, length); +} - for (int i = 0; i < length; i++) +QByteArray +NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key) +{ + QByteArray ciphertext(plaintext.size(), 0); + + for (int i = 0; i < plaintext.size(); i += 16) { - array.data()[i] = static_cast(QRandomGenerator::global()->bounded(256)); + AES_encrypt(reinterpret_cast(&plaintext.data()[i]), + reinterpret_cast(&ciphertext.data()[i]), + key); } - return array; + 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(&ciphertext.data()[i]), + reinterpret_cast(&plaintext.data()[i]), + key); + } + + return plaintext; +} + +bool +NvPairingManager::verifySignature(QByteArray data, QByteArray signature) +{ + EVP_PKEY* pubKey = X509_get_pubkey(m_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(signature.data()), signature.length()); + + EVP_PKEY_free(pubKey); + EVP_MD_CTX_destroy(mdctx); + + 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(message.data()), message.length()); + + size_t signatureLength = 0; + EVP_DigestSignFinal(ctx, NULL, &signatureLength); + + QByteArray signature(signatureLength, 0); + EVP_DigestSignFinal(ctx, reinterpret_cast(signature.data()), &signatureLength); + + EVP_MD_CTX_destroy(ctx); + + return signature; } QByteArray NvPairingManager::saltPin(QByteArray salt, QString pin) { - return QByteArray().append(salt).append(pin.toStdString().c_str()); + return QByteArray().append(salt).append(pin.toLatin1()); } NvPairingManager::PairState @@ -42,26 +141,109 @@ NvPairingManager::pair(QString serverInfo, QString pin) 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(QCryptographicHash::hash(saltedPin, hashAlgo).data(), 128, &decKey); - AES_set_encrypt_key(QCryptographicHash::hash(saltedPin, hashAlgo).data(), 128, &encKey); + AES_set_decrypt_key(reinterpret_cast(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey); + AES_set_encrypt_key(reinterpret_cast(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=" + ) + "devicename=roth&updateState=1&phrase=getservercert&salt=" + + salt.toHex() + "&clientcert=" + m_Im.getCertificate().toHex(), + false); + if (m_Http.getXmlString(getCert, "paired") != "1") + { + qDebug() << "Failed pairing at stage #1"; + return PairState::FAILED; + } + + QByteArray encryptedChallenge = encrypt(generateRandomBytes(16), &encKey); + QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, + "pair", + "devicename=roth&updateState=1&clientchallenge=" + + encryptedChallenge.toHex(), + true); + if (m_Http.getXmlString(challengeXml, "paired") != "1") + { + qDebug() << "Failed pairing at stage #2"; + return PairState::FAILED; + } + + QByteArray challengeResponseData = decrypt( + QByteArray::fromHex(m_Http.getXmlString(challengeXml, "challengeresponse").toLatin1()), + &decKey); + QByteArray clientSecretData = generateRandomBytes(16); + QByteArray challengeResponse; + + const ASN1_BIT_STRING *asnSignature; + X509_get0_signature(&asnSignature, NULL, m_Cert); + + challengeResponse.append(challengeResponseData.data() + hashLength, 16); + challengeResponse.append(reinterpret_cast(asnSignature->data), 256); + 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); + if (m_Http.getXmlString(respXml, "paired") != "1") + { + qDebug() << "Failed pairing at stage #3"; + return PairState::FAILED; + } + + QByteArray pairingSecret = + QByteArray::fromHex(m_Http.getXmlString(challengeXml, "pairingsecret").toLatin1()); + + if (!verifySignature(QByteArray(pairingSecret.data(), 16), + QByteArray(&pairingSecret.data()[16], 256))) + { + qDebug() << "MITM detected"; + return PairState::FAILED; + } + + 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); + if (m_Http.getXmlString(secretRespXml, "paired") != "1") + { + qDebug() << "Failed pairing at stage #4"; + return PairState::FAILED; + } + + QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps, + "pair", + "devicename=roth&updateState=1&phase=pairchallenge", + true); + if (m_Http.getXmlString(pairChallengeXml, "paired") != "1") + { + qDebug() << "Failed pairing at stage #5"; + return PairState::FAILED; + } + + return PairState::PAIRED; } diff --git a/nvpairingmanager.h b/nvpairingmanager.h index d5ac80ed..455fbe43 100644 --- a/nvpairingmanager.h +++ b/nvpairingmanager.h @@ -1,7 +1,12 @@ #pragma once +#include #include +#include +#include +#include + class NvPairingManager { public: @@ -14,7 +19,9 @@ public: ALREADY_IN_PROGRESS }; - NvPairingManager(QString address); + NvPairingManager(QString address, IdentityManager im); + + ~NvPairingManager(); QString generatePinString(); @@ -23,5 +30,26 @@ public: 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); + + bool + verifySignature(QByteArray data, QByteArray signature); + + QByteArray + signMessage(QByteArray message); + NvHTTP m_Http; + IdentityManager m_Im; + X509* m_Cert; + EVP_PKEY* m_PrivateKey; }; diff --git a/utils.h b/utils.h new file mode 100644 index 00000000..c5b2897c --- /dev/null +++ b/utils.h @@ -0,0 +1,4 @@ +#pragma once + +#define THROW_BAD_ALLOC_IF_NULL(x) \ + if ((x) == nullptr) throw new std::bad_alloc()