mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2025-07-02 07:46:07 +00:00
Merging master
This commit is contained in:
commit
23b3fb5af0
132
identitymanager.cpp
Normal file
132
identitymanager.cpp
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
#include "identitymanager.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QSslCertificate>
|
||||||
|
#include <QSslKey>
|
||||||
|
|
||||||
|
#include <openssl/pem.h>
|
||||||
|
#include <openssl/rsa.h>
|
||||||
|
#include <openssl/pkcs12.h>
|
||||||
|
|
||||||
|
IdentityManager::IdentityManager(QDir directory)
|
||||||
|
{
|
||||||
|
QFile uniqueIdFile(directory.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);
|
||||||
|
|
||||||
|
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
identitymanager.h
Normal file
25
identitymanager.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
class IdentityManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IdentityManager(QDir directory);
|
||||||
|
|
||||||
|
QString
|
||||||
|
getUniqueId();
|
||||||
|
|
||||||
|
QByteArray
|
||||||
|
getCertificate();
|
||||||
|
|
||||||
|
QByteArray
|
||||||
|
getPrivateKey();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QDir m_RootDirectory;
|
||||||
|
|
||||||
|
QByteArray m_CachedPrivateKey;
|
||||||
|
QByteArray m_CachedPemCert;
|
||||||
|
QString m_CachedUniqueId;
|
||||||
|
};
|
@ -20,20 +20,29 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
|||||||
# You can also make your code fail to compile if you use deprecated APIs.
|
# You can also make your code fail to compile if you use deprecated APIs.
|
||||||
# In order to do so, uncomment the following line.
|
# 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.
|
# 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 += \
|
SOURCES += \
|
||||||
main.cpp \
|
main.cpp \
|
||||||
mainwindow.cpp \
|
mainwindow.cpp \
|
||||||
nvhttp.cpp
|
nvhttp.cpp \
|
||||||
|
nvpairingmanager.cpp \
|
||||||
|
identitymanager.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
mainwindow.h \
|
mainwindow.h \
|
||||||
nvhttp.h
|
nvhttp.h \
|
||||||
|
nvpairingmanager.h \
|
||||||
|
identitymanager.h \
|
||||||
|
utils.h
|
||||||
|
|
||||||
FORMS += \
|
FORMS += \
|
||||||
mainwindow.ui
|
mainwindow.ui
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
resources.qrc
|
resources.qrc
|
||||||
|
|
||||||
|
LIBS += \
|
||||||
|
-lssl \
|
||||||
|
-lcrypto
|
||||||
|
84
nvhttp.cpp
84
nvhttp.cpp
@ -1,15 +1,18 @@
|
|||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
#include <QtNetwork/QNetworkReply>
|
#include <QtNetwork/QNetworkReply>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QXmlStreamReader>
|
#include <QXmlStreamReader>
|
||||||
|
#include <QSslKey>
|
||||||
|
|
||||||
#define REQUEST_TIMEOUT_MS 5000
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
|
||||||
NvHTTP::NvHTTP(QString address) :
|
NvHTTP::NvHTTP(QString address, IdentityManager im) :
|
||||||
m_Address(address)
|
m_Address(address),
|
||||||
|
m_Im(im)
|
||||||
{
|
{
|
||||||
m_BaseUrlHttp.setScheme("http");
|
m_BaseUrlHttp.setScheme("http");
|
||||||
m_BaseUrlHttps.setScheme("https");
|
m_BaseUrlHttps.setScheme("https");
|
||||||
@ -58,6 +61,21 @@ NvHTTP::getComputerInfo()
|
|||||||
return computer;
|
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
|
int
|
||||||
NvHTTP::getCurrentGame(QString serverInfo)
|
NvHTTP::getCurrentGame(QString serverInfo)
|
||||||
{
|
{
|
||||||
@ -65,10 +83,12 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
|||||||
// has the semantics that its name would indicate. To contain the effects of this change as much
|
// 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.
|
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
||||||
QString serverState = getXmlString(serverInfo, "state");
|
QString serverState = getXmlString(serverInfo, "state");
|
||||||
if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY")) {
|
if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY"))
|
||||||
|
{
|
||||||
return getXmlString(serverInfo, "currentgame").toInt();
|
return getXmlString(serverInfo, "currentgame").toInt();
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,17 +109,22 @@ NvHTTP::getServerInfo()
|
|||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
catch (GfeHttpResponseException& e)
|
catch (const GfeHttpResponseException& e)
|
||||||
{
|
{
|
||||||
if (e.getStatusCode() == 401)
|
if (e.getStatusCode() == 401)
|
||||||
{
|
{
|
||||||
// Certificate validation error, fallback to HTTP
|
// Certificate validation error, fallback to HTTP
|
||||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true);
|
true);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Rethrow real errors
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverInfo;
|
return serverInfo;
|
||||||
@ -130,17 +155,35 @@ NvHTTP::verifyResponseStatus(QString xml)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QByteArray
|
||||||
|
NvHTTP::getXmlStringFromHex(QString xml,
|
||||||
|
QString tagName)
|
||||||
|
{
|
||||||
|
QString str = getXmlString(xml, tagName);
|
||||||
|
if (str == nullptr)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QByteArray::fromHex(str.toLatin1());
|
||||||
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
NvHTTP::getXmlString(QString xml,
|
NvHTTP::getXmlString(QString xml,
|
||||||
QString tagName)
|
QString tagName)
|
||||||
{
|
{
|
||||||
QXmlStreamReader xmlReader(xml);
|
QXmlStreamReader xmlReader(xml);
|
||||||
|
|
||||||
while (xmlReader.readNextStartElement())
|
while (!xmlReader.atEnd())
|
||||||
{
|
{
|
||||||
|
if (xmlReader.readNext() != QXmlStreamReader::StartElement)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (xmlReader.name() == tagName)
|
if (xmlReader.name() == tagName)
|
||||||
{
|
{
|
||||||
return xmlReader.text().toString();
|
return xmlReader.readElementText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,15 +213,23 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
{
|
{
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
QUrl url(baseUrl);
|
QUrl url(baseUrl);
|
||||||
url.setPath(command +
|
url.setPath("/" + command);
|
||||||
"?uniqueid=" + "0" +
|
url.setQuery("uniqueid=" + m_Im.getUniqueId() +
|
||||||
"&uuid=" + QUuid::createUuid().toString() +
|
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
|
||||||
((arguments != nullptr) ? (arguments + "&") : ""));
|
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||||
|
|
||||||
QNetworkReply* reply = m_Nam.get(QNetworkRequest(url));
|
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)
|
// Ignore self-signed certificate errors (since GFE uses them)
|
||||||
reply->ignoreSslErrors(QList<QSslError>{ QSslError::SelfSignedCertificate });
|
reply->ignoreSslErrors();
|
||||||
|
|
||||||
// Run the request with a timeout if requested
|
// Run the request with a timeout if requested
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
@ -200,9 +251,10 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Handle error
|
// Handle error
|
||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
qDebug() << command << " request for failed with error " << reply->error();
|
qDebug() << command << " request failed with error " << reply->error();
|
||||||
|
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
||||||
delete reply;
|
delete reply;
|
||||||
throw new GfeHttpResponseException(reply->error(), reply->errorString());
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
|
36
nvhttp.h
36
nvhttp.h
@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "identitymanager.h"
|
||||||
|
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QtNetwork/QNetworkAccessManager>
|
#include <QtNetwork/QNetworkAccessManager>
|
||||||
|
|
||||||
@ -15,15 +17,15 @@ public:
|
|||||||
|
|
||||||
const char* what() const throw()
|
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;
|
return m_StatusCode;
|
||||||
}
|
}
|
||||||
@ -65,14 +67,7 @@ public:
|
|||||||
class NvHTTP
|
class NvHTTP
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
NvHTTP(QString address);
|
NvHTTP(QString address, IdentityManager im);
|
||||||
|
|
||||||
private:
|
|
||||||
QNetworkReply*
|
|
||||||
openConnection(QUrl baseUrl,
|
|
||||||
QString command,
|
|
||||||
QString arguments,
|
|
||||||
bool enableTimeout);
|
|
||||||
|
|
||||||
NvComputer
|
NvComputer
|
||||||
getComputerInfo();
|
getComputerInfo();
|
||||||
@ -90,14 +85,29 @@ private:
|
|||||||
getXmlString(QString xml,
|
getXmlString(QString xml,
|
||||||
QString tagName);
|
QString tagName);
|
||||||
|
|
||||||
|
QByteArray
|
||||||
|
getXmlStringFromHex(QString xml,
|
||||||
|
QString tagName);
|
||||||
|
|
||||||
QString
|
QString
|
||||||
openConnectionToString(QUrl baseUrl,
|
openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout);
|
bool enableTimeout);
|
||||||
|
|
||||||
QString m_Address;
|
QVector<int>
|
||||||
|
getServerVersionQuad(QString serverInfo);
|
||||||
|
|
||||||
QUrl m_BaseUrlHttp;
|
QUrl m_BaseUrlHttp;
|
||||||
QUrl m_BaseUrlHttps;
|
QUrl m_BaseUrlHttps;
|
||||||
|
private:
|
||||||
|
QNetworkReply*
|
||||||
|
openConnection(QUrl baseUrl,
|
||||||
|
QString command,
|
||||||
|
QString arguments,
|
||||||
|
bool enableTimeout);
|
||||||
|
|
||||||
|
QString m_Address;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager m_Nam;
|
||||||
|
IdentityManager m_Im;
|
||||||
};
|
};
|
||||||
|
308
nvpairingmanager.cpp
Normal file
308
nvpairingmanager.cpp
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
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
|
||||||
|
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);
|
||||||
|
|
||||||
|
const ASN1_BIT_STRING *asnSignature;
|
||||||
|
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);
|
||||||
|
|
||||||
|
const ASN1_BIT_STRING *asnSignature;
|
||||||
|
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
nvpairingmanager.h
Normal file
58
nvpairingmanager.h
Normal 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;
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user