diff --git a/CMakeLists.txt b/CMakeLists.txt index 672e766..fae6aec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,12 +117,30 @@ endif() add_subdirectory(Bootstrap) add_subdirectory(Common) + +add_library(UpdateClientUpdaterLogic STATIC + Updater/UpdaterLogic.h + Updater/UpdaterLogic.cpp +) +target_compile_features(UpdateClientUpdaterLogic PUBLIC cxx_std_17) +target_include_directories(UpdateClientUpdaterLogic PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/Updater" +) +target_link_libraries(UpdateClientUpdaterLogic + PUBLIC + Common + Qt5::Core + Qt5::Network + Qt5::Gui + Qt5::Widgets +) + add_subdirectory(Launcher) add_subdirectory(Updater) add_subdirectory(MainApp) foreach(_update_client_primary_target IN ITEMS - Bootstrap Common Launcher Updater MainApp) + Bootstrap Common UpdateClientUpdaterLogic Launcher Updater MainApp) add_dependencies("${_update_client_primary_target}" UpdateClientNoHanCheck) endforeach() diff --git a/Common/CMakeLists.txt b/Common/CMakeLists.txt index 0e510a6..8f97862 100644 --- a/Common/CMakeLists.txt +++ b/Common/CMakeLists.txt @@ -5,6 +5,8 @@ set(_common_sources FileHelper.cpp ConfigHelper.h ConfigHelper.cpp + InitialStatePolicy.h + ManifestBootstrapPolicy.h PolicyHelper.h PolicyHelper.cpp LocalStateHelper.h diff --git a/Common/ConfigHelper.cpp b/Common/ConfigHelper.cpp index 4a7486e..e289f9b 100644 --- a/Common/ConfigHelper.cpp +++ b/Common/ConfigHelper.cpp @@ -1,4 +1,5 @@ #include "ConfigHelper.h" +#include "InitialStatePolicy.h" #include #include @@ -583,9 +584,20 @@ bool ConfigHelper::initializeStateDefaults() for (const QString& key : mutableKeys()) { + if (!m_initialState.contains(key)) + continue; + const QString storageKey = stateStorageKey(key); - if (!settings.contains(storageKey) && m_initialState.contains(key)) - settings.setValue(storageKey, m_initialState.value(key).toString()); + const bool storedValueExists = settings.contains(storageKey); + const QString storedValue = storedValueExists + ? settings.value(storageKey).toString() + : QString(); + const QString initialValue = m_initialState.value(key).toString(); + if (UpdateClient::shouldApplyInitialStateValue( + key, storedValueExists, storedValue, initialValue)) + { + settings.setValue(storageKey, initialValue); + } } settings.sync(); diff --git a/Common/InitialStatePolicy.h b/Common/InitialStatePolicy.h new file mode 100644 index 0000000..d291192 --- /dev/null +++ b/Common/InitialStatePolicy.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace UpdateClient +{ +inline bool shouldApplyInitialStateValue(const QString& key, + bool storedValueExists, + const QString& storedValue, + const QString& initialValue) +{ + if (!storedValueExists) + return true; + + return key == QStringLiteral("license_key") + && storedValue.trimmed().isEmpty() + && !initialValue.trimmed().isEmpty(); +} +} diff --git a/Common/IntegrityHelper.cpp b/Common/IntegrityHelper.cpp index e6b63a1..9abbf16 100644 --- a/Common/IntegrityHelper.cpp +++ b/Common/IntegrityHelper.cpp @@ -19,6 +19,11 @@ IntegrityHelper::IntegrityHelper(const QString& installDir) QString IntegrityHelper::errorString() const { return m_error; } +bool IntegrityHelper::isManifestCacheMissing() const +{ + return m_manifestCacheMissing; +} + bool IntegrityHelper::safeRelativePath(const QString& path) const { const QString clean = QDir::cleanPath(QDir::fromNativeSeparators(path)); @@ -82,14 +87,20 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString const QString& version) { m_error.clear(); + m_manifestCacheMissing = false; QString cachePath = QDir(ConfigHelper::instance().updateRoot()).filePath( "manifest_cache/manifest_" + version + ".json"); const QString legacyCachePath = QDir(m_installDir).filePath( "update/manifest_cache/manifest_" + version + ".json"); if (!QFile::exists(cachePath)) cachePath = legacyCachePath; + if (!QFile::exists(cachePath)) { + m_manifestCacheMissing = true; + m_error = "signed manifest cache missing for " + version; + return false; + } QFile cache(cachePath); - if (!cache.open(QIODevice::ReadOnly)) { m_error = "signed manifest cache missing for " + version; return false; } + if (!cache.open(QIODevice::ReadOnly)) { m_error = "cannot read signed manifest cache for " + version; return false; } QJsonParseError wrapperError; const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError); if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { diff --git a/Common/IntegrityHelper.h b/Common/IntegrityHelper.h index 202bfc7..4e407b1 100644 --- a/Common/IntegrityHelper.h +++ b/Common/IntegrityHelper.h @@ -7,6 +7,7 @@ public: explicit IntegrityHelper(const QString& installDir); bool verifyInstalledVersion(const QString& appId, const QString& channel, const QString& version); + bool isManifestCacheMissing() const; QString errorString() const; private: @@ -17,4 +18,5 @@ private: QString m_installDir; QString m_error; + bool m_manifestCacheMissing = false; }; diff --git a/Common/ManifestBootstrapPolicy.h b/Common/ManifestBootstrapPolicy.h new file mode 100644 index 0000000..26ac2ef --- /dev/null +++ b/Common/ManifestBootstrapPolicy.h @@ -0,0 +1,24 @@ +#pragma once + +namespace UpdateClient +{ +enum class ManifestBootstrapAction +{ + None, + FetchCurrentManifest, + CurrentVersionNotPublished +}; + +inline ManifestBootstrapAction manifestBootstrapAction( + bool manifestCacheMissing, + bool networkAvailable, + bool updateAvailable, + int serverVersionId) +{ + if (!manifestCacheMissing || !networkAvailable || updateAvailable) + return ManifestBootstrapAction::None; + if (serverVersionId <= 0) + return ManifestBootstrapAction::CurrentVersionNotPublished; + return ManifestBootstrapAction::FetchCurrentManifest; +} +} diff --git a/Launcher/CMakeLists.txt b/Launcher/CMakeLists.txt index 1ed43b9..dd532dd 100644 --- a/Launcher/CMakeLists.txt +++ b/Launcher/CMakeLists.txt @@ -17,6 +17,7 @@ target_compile_features(Launcher PRIVATE cxx_std_17) target_link_libraries(Launcher PRIVATE Common + UpdateClientUpdaterLogic Qt5::Core Qt5::Network Qt5::Gui diff --git a/Launcher/main.cpp b/Launcher/main.cpp index 7ec5f72..db746ef 100644 --- a/Launcher/main.cpp +++ b/Launcher/main.cpp @@ -12,7 +12,9 @@ #include "../Common/TicketHelper.h" #include "../Common/DeviceIdentityHelper.h" #include "../Common/IntegrityHelper.h" +#include "../Common/ManifestBootstrapPolicy.h" #include "../Common/TranslationHelper.h" +#include "../Updater/UpdaterLogic.h" #include #include #include @@ -198,7 +200,47 @@ int main(int argc, char *argv[]) const QString mainAppPath = QDir(appDir).filePath(mainExecutable); const QString updaterPath = QDir(appDir).filePath(updaterExecutable); IntegrityHelper integrity(config.installRoot()); - if (!integrity.verifyInstalledVersion(appId, channel, currentVersion)) + bool integrityVerified = integrity.verifyInstalledVersion( + appId, channel, currentVersion); + QString integrityError = integrity.errorString(); + const UpdateClient::ManifestBootstrapAction bootstrapAction = + UpdateClient::manifestBootstrapAction( + integrity.isManifestCacheMissing(), networkOk, needUpdate, + targetVersionId); + if (!integrityVerified + && bootstrapAction + == UpdateClient::ManifestBootstrapAction::FetchCurrentManifest) + { + UpdaterLogic manifestLogic; + manifestLogic.getManifest( + appId, channel, currentVersion, targetVersionId); + const QString cacheDirectory = QDir(config.updateRoot()).filePath( + QStringLiteral("manifest_cache")); + if (manifestLogic.verifyManifestSignature() + && manifestLogic.saveManifestCache(cacheDirectory)) + { + integrityVerified = integrity.verifyInstalledVersion( + appId, channel, currentVersion); + integrityError = integrity.errorString(); + } + else + { + integrityError = QCoreApplication::translate( + "Launcher", + "The signed manifest for version %1 could not be fetched or cached.") + .arg(currentVersion); + } + } + else if (!integrityVerified + && bootstrapAction + == UpdateClient::ManifestBootstrapAction::CurrentVersionNotPublished) + { + integrityError = QCoreApplication::translate( + "Launcher", + "Version %1 is not published on the update server. Publish this exact installed build before distribution so its signed manifest can be verified.") + .arg(currentVersion); + } + if (!integrityVerified) { progress.close(); QMessageBox::critical( @@ -207,7 +249,7 @@ int main(int argc, char *argv[]) QCoreApplication::translate( "Launcher", "Application files failed signed manifest verification:\n%1") - .arg(integrity.errorString())); + .arg(integrityError)); return -1; } const auto importOfflinePackage = [&]() diff --git a/Updater/CMakeLists.txt b/Updater/CMakeLists.txt index 8841b7f..c87ce0e 100644 --- a/Updater/CMakeLists.txt +++ b/Updater/CMakeLists.txt @@ -1,7 +1,5 @@ set(_updater_sources main.cpp - UpdaterLogic.h - UpdaterLogic.cpp UpdateTransaction.h UpdateTransaction.cpp ) @@ -11,6 +9,7 @@ target_compile_features(Updater PRIVATE cxx_std_17) target_link_libraries(Updater PRIVATE Common + UpdateClientUpdaterLogic Qt5::Core Qt5::Network Qt5::Gui diff --git a/docs/architecture.md b/docs/architecture.md index 0bbf294..845ed3f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -118,6 +118,8 @@ 带签名的 JSON 合约必须使用确定性序列化,从已签名的负载中省略签名字段,标识算法和密钥 ID,并支持受控的公钥轮换窗口。验证失败将导致系统关闭。 +首次安装可能尚无当前版本的本地清单缓存。当设备在线、当前版本已在服务器显式发布且没有更高目标版本时,Launcher 会通过与 Updater 相同的校验逻辑获取当前版本清单,验证签名和身份后写入缓存,再执行完整文件校验。服务器未发布当前版本、返回无效版本 ID、清单签名无效或缓存写入失败时,启动保持关闭失败。此流程不会自动创建或发布服务器版本。 + ## 运行时布局 父安装程序拥有最终的布局。预期的可执行文件关系为: @@ -157,4 +159,4 @@ config/ * 频道选择和禁止降级行为; * 中断更新恢复; * 候选失败和成功回滚; -* 从实际安装位置启动并更新,通过正常的最终用户交互并需要提升权限。 \ No newline at end of file +* 从实际安装位置启动并更新,通过正常的最终用户交互并需要提升权限。 diff --git a/docs/configuration-and-security.md b/docs/configuration-and-security.md index 98bfbcc..3ca9cb6 100644 --- a/docs/configuration-and-security.md +++ b/docs/configuration-and-security.md @@ -37,7 +37,7 @@ 所选的源也包含一个 `initial_state` 对象。它仅提供以下可变键的首次运行默认值。它不会将这些键变为不可变策略,之后的运行时更改仍保留在本地状态存储中。 -发布者必须在未跟踪的 `config/config.json` 中提供已签发且非空的 `initial_state.license_key`。示例配置中的空值只支持开发构建;父级 `package_installer` 会拒绝缺失、空值或明显占位符。该值会作为首次运行默认状态嵌入二进制文件,并由 Launcher 写入系统级状态,不会作为可编辑的安装目录 JSON 交付。 +发布者必须在未跟踪的 `config/config.json` 中提供已签发且非空的 `initial_state.license_key`。示例配置中的空值只支持开发构建;父级 `package_installer` 会拒绝缺失、空值或明显占位符。该值会作为初始状态嵌入二进制文件,并在系统级授权状态缺失或为空时由 Launcher 写入;已有的非空机器授权不会被构建默认值覆盖。授权不会作为可编辑的安装目录 JSON 交付。 这些值可能在不同构建之间有所不同,但安装的文件不得覆盖它们。在可执行文件旁边放置名为 `app_config.json`、`client.ini` 或 `config.json` 的文件,不是受支持的运行时覆盖。 diff --git a/translations/update-client_zh_CN.ts b/translations/update-client_zh_CN.ts index 9d27899..5e84c05 100644 --- a/translations/update-client_zh_CN.ts +++ b/translations/update-client_zh_CN.ts @@ -225,6 +225,14 @@ Target version: %1 应用文件未通过签名清单验证: %1 + + The signed manifest for version %1 could not be fetched or cached. + 无法获取或缓存版本 %1 的签名清单。 + + + Version %1 is not published on the update server. Publish this exact installed build before distribution so its signed manifest can be verified. + 版本 %1 尚未发布到更新服务器。请在分发前发布当前安装构建,以便验证其签名清单。 + LauncherErrorMarker