新增了客户端打包成sdk的功能和服务端docker镜像打包的功能,并修复了一些问题

This commit is contained in:
2026-07-07 09:48:01 +00:00
parent cd3dca042d
commit 9e545cb328
50 changed files with 5251 additions and 875 deletions
+37 -31
View File
@@ -1,32 +1,38 @@
project(Common LANGUAGES C CXX)
find_package(Qt5 REQUIRED COMPONENTS Core Network Widgets)
set(SRC
HttpHelper.h
HttpHelper.cpp
FileHelper.h
FileHelper.cpp
ConfigHelper.h
ConfigHelper.cpp
PolicyHelper.h
PolicyHelper.cpp
LocalStateHelper.h
LocalStateHelper.cpp
)
add_library(Common STATIC ${SRC})
# Common编译自身需要OpenSSL头文件
target_include_directories(Common
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${OPENSSL_INC}
)
# 链接库、库目录通过INTERFACE传递给所有依赖Common的exe
target_link_directories(Common INTERFACE
$<$<CONFIG:Debug>:${OPENSSL_LIB_DEBUG}>
$<$<CONFIG:Release>:${OPENSSL_LIB_RELEASE}>
)
target_link_libraries(Common
PRIVATE Qt5::Core Qt5::Network Qt5::Widgets
INTERFACE libssl.lib libcrypto.lib
project(Common LANGUAGES C CXX)
find_package(Qt5 REQUIRED COMPONENTS Core Network Widgets)
set(SRC
HttpHelper.h
HttpHelper.cpp
FileHelper.h
FileHelper.cpp
ConfigHelper.h
ConfigHelper.cpp
PolicyHelper.h
PolicyHelper.cpp
LocalStateHelper.h
LocalStateHelper.cpp
TicketHelper.h
TicketHelper.cpp
IntegrityHelper.h
IntegrityHelper.cpp
DeviceIdentityHelper.h
DeviceIdentityHelper.cpp
)
add_library(Common STATIC ${SRC})
# Common编译自身需要OpenSSL头文件
target_include_directories(Common
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${OPENSSL_INC}
)
# 链接库、库目录通过INTERFACE传递给所有依赖Common的exe
target_link_directories(Common INTERFACE
$<$<CONFIG:Debug>:${OPENSSL_LIB_DEBUG}>
$<$<CONFIG:Release>:${OPENSSL_LIB_RELEASE}>
)
target_link_libraries(Common
PRIVATE Qt5::Core Qt5::Network Qt5::Widgets
INTERFACE libssl.lib libcrypto.lib
)
+7
View File
@@ -95,12 +95,19 @@ bool ConfigHelper::migrateLegacyIniIfNeeded()
copyText("App", "app_name", "Marsco Demo App");
copyText("App", "channel", "stable");
copyText("App", "current_version", "1.0.0");
copyText("App", "client_protocol", "3");
copyText("App", "launch_token");
copyText("License", "license_key");
copyText("Server", "api_base_url");
copyText("Server", "client_token");
copyText("Update", "request_timeout_ms", "5000");
copyText("Update", "temp_folder", "update_temp");
copyText("Update", "device_id");
copyText("Runtime", "main_executable", "MainApp.exe");
copyText("Runtime", "launcher_executable", "Launcher.exe");
copyText("Runtime", "updater_executable", "Updater.exe");
copyText("Runtime", "bootstrap_executable", "Bootstrap.exe");
copyText("Runtime", "health_check_timeout_ms", "15000");
config.insert("platform", "windows");
config.insert("arch", "x64");
+41
View File
@@ -0,0 +1,41 @@
#include "DeviceIdentityHelper.h"
#include "ConfigHelper.h"
#include <QCryptographicHash>
#include <QDateTime>
#include <QDir>
#include <QEventLoop>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSaveFile>
#include <QSysInfo>
#include <QUuid>
#ifdef HAVE_OPENSSL
#include <openssl/evp.h>
#include <openssl/pem.h>
#endif
DeviceIdentityHelper::DeviceIdentityHelper(const QString& dir):m_installDir(dir){}
QString DeviceIdentityHelper::deviceId() const{return m_deviceId;}
QString DeviceIdentityHelper::errorString() const{return m_error;}
bool DeviceIdentityHelper::verifySignature(const QByteArray& payload,const QString& sig64){
#ifndef HAVE_OPENSSL
Q_UNUSED(payload);Q_UNUSED(sig64);m_error="OpenSSL unavailable";return false;
#else
QFile f(QDir(m_installDir).filePath("config/manifest_public_key.pem")); if(!f.open(QIODevice::ReadOnly)){m_error="device public key missing";return false;}
QByteArray kd=f.readAll();BIO* b=BIO_new_mem_buf(kd.constData(),kd.size());EVP_PKEY* k=b?PEM_read_bio_PUBKEY(b,nullptr,nullptr,nullptr):nullptr;if(b)BIO_free(b);if(!k){m_error="device public key invalid";return false;}
EVP_MD_CTX* c=EVP_MD_CTX_new();QByteArray sig=QByteArray::fromBase64(sig64.toUtf8());bool ok=c&&EVP_DigestVerifyInit(c,nullptr,EVP_sha256(),nullptr,k)==1&&EVP_DigestVerifyUpdate(c,payload.constData(),payload.size())==1&&EVP_DigestVerifyFinal(c,reinterpret_cast<const unsigned char*>(sig.constData()),sig.size())==1;if(c)EVP_MD_CTX_free(c);EVP_PKEY_free(k);if(!ok)m_error="device credential RSA signature invalid";return ok;
#endif
}
bool DeviceIdentityHelper::loadAndVerify(const QString& appId,const QString& channel){
QFile f(QDir(m_installDir).filePath("config/client_identity.dat"));if(!f.open(QIODevice::ReadOnly))return false;QJsonParseError e;auto d=QJsonDocument::fromJson(f.readAll(),&e);if(e.error!=QJsonParseError::NoError||!d.isObject()){m_error="device credential JSON invalid";return false;}auto w=d.object();QByteArray text=w.value("identity_text").toString().toUtf8();if(text.isEmpty()||!verifySignature(text,w.value("signature").toString()))return false;auto identity=QJsonDocument::fromJson(text).object();QDateTime expiry=QDateTime::fromString(identity.value("valid_until").toString(),Qt::ISODate);if(identity.value("app_id").toString()!=appId||identity.value("channel").toString()!=channel||identity.value("license_id").toString().isEmpty()||identity.value("installation_id").toString().isEmpty()||identity.value("device_id").toString().isEmpty()){m_error="device/license credential identity mismatch";return false;}if(!expiry.isValid()||expiry<=QDateTime::currentDateTimeUtc()){m_error="license expired";return false;}m_deviceId=identity.value("device_id").toString();return true;
}
bool DeviceIdentityHelper::verifyLocal(const QString& appId,const QString& channel){m_error.clear();return loadAndVerify(appId,channel);}
bool DeviceIdentityHelper::ensureIssued(const QString& base,const QString& token,const QString& appId,const QString& channel,const QString& licenseKey){
m_error.clear();if(loadAndVerify(appId,channel)){ConfigHelper::instance().setValue("Update","device_id",m_deviceId);return true;}
QString installation=ConfigHelper::instance().getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!ConfigHelper::instance().setValue("Device","installation_id",installation)){m_error="cannot save installation id";return false;}}
QByteArray machine=QSysInfo::machineUniqueId()+installation.toUtf8();QString mh=QString::fromLatin1(QCryptographicHash::hash(machine,QCryptographicHash::Sha256).toHex());QJsonObject body{{"app_id",appId},{"channel",channel},{"license_key",licenseKey},{"installation_id",installation},{"machine_hash",mh}};
QNetworkAccessManager manager;QNetworkRequest req{QUrl(base+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);loop.exec();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QSaveFile out(path);QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);if(!out.open(QIODevice::WriteOnly)||out.write(bytes)!=bytes.size()||!out.commit()){m_error="cannot save device credential";return false;}if(!loadAndVerify(appId,channel))return false;if(!ConfigHelper::instance().setValue("Update","device_id",m_deviceId)){m_error="cannot save server device id";return false;}return true;
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <QString>
class DeviceIdentityHelper {
public:
explicit DeviceIdentityHelper(const QString& installDir);
bool ensureIssued(const QString& apiBaseUrl, const QString& clientToken, const QString& appId,
const QString& channel, const QString& licenseKey);
bool verifyLocal(const QString& appId, const QString& channel);
QString deviceId() const;
QString errorString() const;
private:
bool loadAndVerify(const QString& expectedAppId, const QString& expectedChannel);
bool verifySignature(const QByteArray& payload, const QString& signatureBase64);
QString m_installDir, m_deviceId, m_error;
};
+52 -48
View File
@@ -1,49 +1,53 @@
#include "HttpHelper.h"
#include <QNetworkProxy>
#include "ConfigHelper.h"
void HttpHelper::postRequest(const QString& url, const QJsonObject& jsonBody,
std::function<void(int code, const QJsonObject& resp)> callback)
{
QNetworkAccessManager* manager = new QNetworkAccessManager();
manager->setProxy(QNetworkProxy::NoProxy);
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
// Add auth token header
QString token = ConfigHelper::instance().getValue("Server", "client_token");
req.setRawHeader("X-Client-Token", token.toUtf8());
QByteArray data = QJsonDocument(jsonBody).toJson(QJsonDocument::Compact);
qDebug() << "=== POST Request ===";
qDebug() << "Url:" << url;
qDebug() << "Body:" << data;
QNetworkReply* reply = manager->post(req, data);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
int retCode = 0;
QJsonObject retObj;
if (reply->error() != QNetworkReply::NoError)
{
qDebug() << "Network error code:" << reply->error();
qDebug() << "Error detail:" << reply->errorString();
}
else
{
retCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QByteArray respData = reply->readAll();
qDebug() << "Server raw response:" << respData;
QJsonDocument doc = QJsonDocument::fromJson(respData);
retObj = doc.object();
}
callback(retCode, retObj);
reply->deleteLater();
manager->deleteLater();
#include "HttpHelper.h"
#include <QNetworkProxy>
#include "ConfigHelper.h"
#include <QFile>
#include <QDir>
#include <QApplication>
void HttpHelper::postRequest(const QString& url, const QJsonObject& jsonBody,
std::function<void(int code, const QJsonObject& resp)> callback)
{
QNetworkAccessManager* manager = new QNetworkAccessManager();
manager->setProxy(QNetworkProxy::NoProxy);
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
// Add auth token header
QString token = ConfigHelper::instance().getValue("Server", "client_token");
req.setRawHeader("X-Client-Token", token.toUtf8());
QFile identity(QDir(QApplication::applicationDirPath()).filePath("config/client_identity.dat"));
if (identity.open(QIODevice::ReadOnly))
req.setRawHeader("X-Device-Credential", identity.readAll().toBase64());
QByteArray data = QJsonDocument(jsonBody).toJson(QJsonDocument::Compact);
qDebug() << "=== POST Request ===";
qDebug() << "Url:" << url;
qDebug() << "Body:" << data;
QNetworkReply* reply = manager->post(req, data);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
int retCode = 0;
QJsonObject retObj;
retCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
const QByteArray respData = reply->readAll();
if (!respData.isEmpty()) {
qDebug() << "Server raw response:" << respData;
retObj = QJsonDocument::fromJson(respData).object();
}
if (reply->error() != QNetworkReply::NoError)
{
qDebug() << "Network error code:" << reply->error();
qDebug() << "HTTP status:" << retCode << "detail:" << reply->errorString();
}
callback(retCode, retObj);
reply->deleteLater();
manager->deleteLater();
}
+132
View File
@@ -0,0 +1,132 @@
#include "IntegrityHelper.h"
#include <QCryptographicHash>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#ifdef HAVE_OPENSSL
#include <openssl/evp.h>
#include <openssl/pem.h>
#endif
IntegrityHelper::IntegrityHelper(const QString& installDir)
: m_installDir(QDir::cleanPath(installDir)) {}
QString IntegrityHelper::errorString() const { return m_error; }
bool IntegrityHelper::safeRelativePath(const QString& path) const
{
const QString clean = QDir::cleanPath(QDir::fromNativeSeparators(path));
return !clean.isEmpty() && !QDir::isAbsolutePath(clean) && clean != ".."
&& !clean.startsWith("../") && !clean.contains(":");
}
bool IntegrityHelper::runtimeProtectedPath(const QString& path) const
{
const QString p = QDir::fromNativeSeparators(path).toCaseFolded();
static const QSet<QString> protectedPaths{
"bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
"config/client_identity.dat", "config/version_policy.dat"
};
return protectedPaths.contains(p);
}
QString IntegrityHelper::sha256(const QString& filePath) const
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) return {};
QCryptographicHash hash(QCryptographicHash::Sha256);
while (!file.atEnd()) hash.addData(file.read(1024 * 1024));
return QString::fromLatin1(hash.result().toHex());
}
bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64)
{
#ifndef HAVE_OPENSSL
Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false;
#else
QFile keyFile(QDir(m_installDir).filePath("config/manifest_public_key.pem"));
if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "manifest public key missing"; return false; }
const QByteArray keyData = keyFile.readAll();
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr;
if (bio) BIO_free(bio);
if (!key) { m_error = "manifest public key invalid"; return false; }
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
const bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1
&& EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size()) == 1
&& EVP_DigestVerifyFinal(ctx, reinterpret_cast<const unsigned char*>(signature.constData()), signature.size()) == 1;
if (ctx) EVP_MD_CTX_free(ctx);
EVP_PKEY_free(key);
if (!ok) m_error = "manifest RSA signature invalid";
return ok;
#endif
}
bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString& channel,
const QString& version)
{
m_error.clear();
const QString cachePath = QDir(m_installDir).filePath(
"update/manifest_cache/manifest_" + version + ".json");
QFile cache(cachePath);
if (!cache.open(QIODevice::ReadOnly)) { m_error = "signed manifest cache missing for " + version; return false; }
QJsonParseError wrapperError;
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError);
if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) {
m_error = "manifest cache JSON invalid"; return false;
}
const QJsonObject wrapper = wrapperDoc.object();
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
const QString signature = wrapper.value("manifest").toObject().value("signature").toString();
if (manifestText.isEmpty() || signature.isEmpty() || !verifySignature(manifestText, signature)) return false;
QJsonParseError manifestError;
const QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestText, &manifestError);
if (manifestError.error != QJsonParseError::NoError || !manifestDoc.isObject()) {
m_error = "signed manifest payload invalid"; return false;
}
const QJsonObject manifest = manifestDoc.object();
if (manifest.value("app_id").toString() != appId
|| manifest.value("channel").toString() != channel
|| manifest.value("version").toString() != version) {
m_error = "manifest identity does not match local application"; return false;
}
QSet<QString> declaredExecutables;
for (const QJsonValue& value : manifest.value("files").toArray()) {
const QJsonObject item = value.toObject();
const QString path = QDir::fromNativeSeparators(item.value("path").toString());
if (!safeRelativePath(path)) { m_error = "unsafe manifest path: " + path; return false; }
if (runtimeProtectedPath(path)) continue;
const QString fullPath = QDir(m_installDir).filePath(path);
if (!QFile::exists(fullPath)) { m_error = "required file missing: " + path; return false; }
const QString expected = item.value("sha256").toString();
const QString actual = sha256(fullPath);
if (actual.isEmpty() || actual.compare(expected, Qt::CaseInsensitive) != 0) {
m_error = "file hash mismatch: " + path; return false;
}
const QString suffix = QFileInfo(path).suffix().toCaseFolded();
if (suffix == "exe" || suffix == "dll") declaredExecutables.insert(path.toCaseFolded());
}
QDir root(m_installDir);
QDirIterator it(m_installDir, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
const QString fullPath = it.next();
const QString relative = QDir::fromNativeSeparators(root.relativeFilePath(fullPath));
const QString folded = relative.toCaseFolded();
if (folded.startsWith("update/") || folded.startsWith("update_temp/")
|| runtimeProtectedPath(relative)) continue;
const QString suffix = QFileInfo(relative).suffix().toCaseFolded();
if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) {
m_error = "undeclared executable or plugin: " + relative; return false;
}
}
return true;
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <QString>
class IntegrityHelper
{
public:
explicit IntegrityHelper(const QString& installDir);
bool verifyInstalledVersion(const QString& appId, const QString& channel,
const QString& version);
QString errorString() const;
private:
bool verifySignature(const QByteArray& payload, const QString& signatureBase64);
bool safeRelativePath(const QString& path) const;
bool runtimeProtectedPath(const QString& path) const;
QString sha256(const QString& filePath) const;
QString m_installDir;
QString m_error;
};
+1
View File
@@ -105,6 +105,7 @@ bool PolicyHelper::isVersionAllowed(const QString& version) const {
}
bool PolicyHelper::allowRun() const { return isValid() && m_policy.value("allow_run").toBool(); }
bool PolicyHelper::forceUpdate() const { return isValid() && m_policy.value("force_update").toBool(); }
bool PolicyHelper::allowRollback() const { return isValid() && m_policy.value("allow_rollback").toBool(); }
bool PolicyHelper::isOfflineAllowed() const { return isValid() && m_policy.value("offline_allowed").toBool(); }
bool PolicyHelper::isExpired() const {
if (!isValid()) return true;
+1
View File
@@ -15,6 +15,7 @@ public:
bool isVersionAllowed(const QString& currentVersion) const;
bool allowRun() const;
bool forceUpdate() const;
bool allowRollback() const;
bool isOfflineAllowed() const;
bool isExpired() const;
qint64 policySeq() const;
+91
View File
@@ -0,0 +1,91 @@
#include "TicketHelper.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageAuthenticationCode>
#include <QSaveFile>
#include <QStandardPaths>
#include <QUuid>
static QByteArray ticketMac(const QJsonObject& payload, const QString& secret)
{
return QMessageAuthenticationCode::hash(
QJsonDocument(payload).toJson(QJsonDocument::Compact), secret.toUtf8(), QCryptographicHash::Sha256).toHex();
}
bool TicketHelper::createTicket(const QString& appId, const QString& deviceId,
const QString& version, const QString& secret,
QString* ticketPath, QString* errorMessage)
{
if (appId.isEmpty() || version.isEmpty() || secret.isEmpty()) {
if (errorMessage) *errorMessage = "ticket identity or secret is empty";
return false;
}
const QDateTime now = QDateTime::currentDateTimeUtc();
QJsonObject payload{
{"app_id", appId}, {"device_id", deviceId}, {"version", version},
{"nonce", QUuid::createUuid().toString(QUuid::WithoutBraces)},
{"issued_at", now.toString(Qt::ISODate)},
{"expires_at", now.addSecs(60).toString(Qt::ISODate)},
{"signature_alg", "HMAC-SHA256"}
};
QJsonObject wrapper{{"payload", payload}, {"signature", QString::fromLatin1(ticketMac(payload, secret))}};
const QString dirPath = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).filePath("marsco_tickets");
if (!QDir().mkpath(dirPath)) { if (errorMessage) *errorMessage = "cannot create ticket directory"; return false; }
const QString path = QDir(dirPath).filePath("ticket_" + QUuid::createUuid().toString(QUuid::WithoutBraces) + ".json");
QSaveFile file(path);
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact);
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
if (errorMessage) *errorMessage = "cannot save ticket";
return false;
}
QFile::setPermissions(path, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
if (ticketPath) *ticketPath = path;
return true;
}
bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& expectedAppId,
const QString& expectedDeviceId, const QString& expectedVersion,
const QString& secret, QString* errorMessage)
{
const QString consumingPath = ticketPath + ".consuming."
+ QString::number(QCoreApplication::applicationPid());
if (!QFile::rename(ticketPath, consumingPath)) {
if (errorMessage) *errorMessage = "ticket missing or already consumed";
return false;
}
QFile file(consumingPath);
if (!file.open(QIODevice::ReadOnly)) {
QFile::remove(consumingPath);
if (errorMessage) *errorMessage = "cannot read claimed ticket";
return false;
}
const QByteArray raw = file.readAll(); file.close();
QFile::remove(consumingPath); // 一次性消费;无论成功失败都不能重放。
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
if (errorMessage) *errorMessage = "invalid ticket JSON"; return false;
}
const QJsonObject wrapper = doc.object();
const QJsonObject payload = wrapper.value("payload").toObject();
const QByteArray actual = wrapper.value("signature").toString().toLatin1();
const QByteArray expected = ticketMac(payload, secret);
const QDateTime issued = QDateTime::fromString(payload.value("issued_at").toString(), Qt::ISODate);
const QDateTime expires = QDateTime::fromString(payload.value("expires_at").toString(), Qt::ISODate);
const QDateTime now = QDateTime::currentDateTimeUtc();
const bool identityOk = payload.value("app_id").toString() == expectedAppId
&& payload.value("device_id").toString() == expectedDeviceId
&& payload.value("version").toString() == expectedVersion;
const bool timeOk = issued.isValid() && expires.isValid() && issued <= now.addSecs(5)
&& expires >= now && issued.secsTo(expires) <= 65;
if (actual.isEmpty() || actual != expected || !identityOk || !timeOk
|| payload.value("nonce").toString().isEmpty()) {
if (errorMessage) *errorMessage = "ticket signature, identity, time or nonce invalid";
return false;
}
return true;
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <QString>
class TicketHelper
{
public:
static bool createTicket(const QString& appId, const QString& deviceId,
const QString& version, const QString& secret,
QString* ticketPath, QString* errorMessage = nullptr);
static bool consumeAndVerify(const QString& ticketPath, const QString& expectedAppId,
const QString& expectedDeviceId, const QString& expectedVersion,
const QString& secret, QString* errorMessage = nullptr);
};