From 8f23fac80b20fdfca0da02659e20a8762da6dbed Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Tue, 6 Oct 2026 12:21:38 +0800 Subject: [PATCH] Remedial measures to enhance the detection of blank key situations --- src-tauri/src/core_config/settings.rs | 36 +++++++++++++++++++-------- src-tauri/src/main.rs | 8 +++--- src-tauri/src/tests/core_config.rs | 34 ++++++++++++++++++++++++- src/i18n/ja.ts | 20 +++++++-------- src/i18n/locales/en.ts | 20 +++++++-------- src/i18n/locales/zh-CN.ts | 20 +++++++-------- 6 files changed, 92 insertions(+), 46 deletions(-) diff --git a/src-tauri/src/core_config/settings.rs b/src-tauri/src/core_config/settings.rs index 167a901..31122eb 100644 --- a/src-tauri/src/core_config/settings.rs +++ b/src-tauri/src/core_config/settings.rs @@ -108,28 +108,35 @@ pub(crate) fn validate_strong_management_secret_key(secret_key: &str) -> Result< Ok(()) } -pub(crate) fn generate_management_secret_key() -> Result { - use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; - - let mut random = [0_u8; 32]; - getrandom::fill(&mut random).map_err(|error| format!("Failed to generate WebUI security key: {error}"))?; - Ok(format!("wui-Aa9_{}", URL_SAFE_NO_PAD.encode(random))) -} - pub(crate) fn management_secret_requires_rotation(secret_key: &str) -> bool { let secret_key = secret_key.trim(); // Empty is an intentional disabled Management API, including after restart. - is_hashed_management_secret_key(secret_key) + !secret_key.is_empty() && validate_strong_management_secret_key(secret_key).is_err() } pub(crate) fn ensure_strong_management_secret(config: &mut GuiConfigFile) -> Result { if !management_secret_requires_rotation(&config.management_secret_key) { return Ok(false); } - config.management_secret_key = generate_management_secret_key()?; + config.management_secret_key = LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY.to_string(); Ok(true) } +// An empty template or a hashed kernel value cannot supply a request credential. +// Preserve a saved plaintext key (including an explicitly disabled API) before +// falling back, so the shared GUI state and the next kernel config use one key. +pub(crate) fn recover_management_secret(imported: &str, saved: Option<&str>) -> String { + if validate_strong_management_secret_key(imported).is_ok() { + return imported.trim().to_string(); + } + if let Some(saved) = saved { + if saved.is_empty() || validate_strong_management_secret_key(saved).is_ok() { + return saved.trim().to_string(); + } + } + LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY.to_string() +} + pub(crate) fn normalize_management_secret_key(secret_key: String) -> Result { let secret_key = secret_key.trim().to_string(); validate_strong_management_secret_key(&secret_key)?; @@ -1337,6 +1344,8 @@ pub(crate) fn load_or_create_gui_config() -> Result { ) }; + let saved_management_secret = presence.management_secret_key.as_deref(); + if gui_config_exists { if let Ok(content) = fs::read_to_string(&config_path) { changed |= [ @@ -1523,7 +1532,12 @@ pub(crate) fn load_or_create_gui_config() -> Result { presence.proxy_override.is_some(), ); config.prefer_gitcode_downloads = config.download_source == VersionDownloadSource::Gitcode; - let management_secret_rotated = ensure_strong_management_secret(&mut config)?; + let recovered_secret = recover_management_secret( + &config.management_secret_key, + saved_management_secret, + ); + let management_secret_rotated = recovered_secret != config.management_secret_key; + config.management_secret_key = recovered_secret; changed |= management_secret_rotated; changed |= sanitize_gui_config(&mut config)?; validate_gui_config(&config)?; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4749924..147e83c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2397,10 +2397,10 @@ fn main() { return; } let mut config = GuiConfigFile::default(); - if let Err(secret_error) = ensure_strong_management_secret(&mut config) { - eprintln!("Failed to initialize WebUI security key: {secret_error}"); - return; - } + // If the GUI configuration cannot be read before the kernel starts, + // use the shared recovery key so requests and the generated kernel + // configuration stay synchronized. + config.management_secret_key = LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY.to_string(); if let Err(sanitize_error) = sanitize_gui_config(&mut config) { eprintln!("Failed to initialize fixed credentials directory: {sanitize_error}"); } diff --git a/src-tauri/src/tests/core_config.rs b/src-tauri/src/tests/core_config.rs index 12456c1..e1f077c 100644 --- a/src-tauri/src/tests/core_config.rs +++ b/src-tauri/src/tests/core_config.rs @@ -150,7 +150,7 @@ fn management_secret_rotation_preserves_disabled_and_custom_values() { ..GuiConfigFile::default() }; assert!(ensure_strong_management_secret(&mut hashed).unwrap()); - assert!(hashed.management_secret_key.starts_with("wui-Aa9_")); + assert_eq!(hashed.management_secret_key, "123456"); let mut custom = GuiConfigFile { management_secret_key: "user-selected-secret".to_string(), @@ -160,6 +160,38 @@ fn management_secret_rotation_preserves_disabled_and_custom_values() { assert_eq!(custom.management_secret_key, "user-selected-secret"); } +#[test] +fn management_secret_recovery_synchronizes_kernel_and_request_credentials() { + let hash = "$2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu"; + for (imported, saved, expected) in [ + ("", None, "123456"), + (hash, None, "123456"), + (hash, Some(hash), "123456"), + ("bad\nkey", None, "123456"), + (hash, Some("known-secret"), "known-secret"), + ("", Some("known-secret"), "known-secret"), + ("", Some(""), ""), + ("new-secret", Some("old-secret"), "new-secret"), + ] { + let config = GuiConfigFile { + management_secret_key: recover_management_secret(imported, saved), + ..GuiConfigFile::default() + }; + assert_eq!(config.management_secret_key, expected); + for (template, section) in [ + ("remote-management: {secret-key: stale}\n", "remote-management"), + ("config-version: 8\nmanagement: {secret-key: stale}\n", "management"), + ] { + let updated = apply_gui_managed_settings(template, &config).unwrap(); + let document: serde_norway::Value = serde_norway::from_str(&updated).unwrap(); + assert_eq!(document[section]["secret-key"], expected); + } + if !expected.is_empty() { + assert_eq!(management_api::management_authorization(&config).unwrap(), format!("Bearer {expected}")); + } + } +} + #[test] fn management_secret_key_is_preserved_and_written_to_core() { let mut config = GuiConfigFile { diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 47457a5..c7b5e2c 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -942,23 +942,23 @@ export const jaOverrides = { 'config.diagnostics.error.nonNegativeInteger': 'ログサイズと保持数には 0 以上の整数を入力してください', 'config.diagnostics.error.redisRetention': '使用量キュー保持時間には 1~3600 の整数を入力してください', 'config.diagnostics.error.openLogs': 'ログフォルダーを開けませんでした:{{error}}', - 'config.webuiKey.title': 'WebUI キー', + 'config.webuiKey.title': '管理キー', 'config.webuiKey.heading': '管理 API のアクセス認証情報', - 'config.webuiKey.description': 'CPA WebUI へのログインと管理 API の呼び出しに使用します。上の API 認証キーとは用途が異なります。', + 'config.webuiKey.description': 'CPA Web 管理画面へのログインと管理 API の呼び出しに使用します。上の API 認証キーとは用途が異なります。', 'config.webuiKey.securityHint': '既存のキーは表示されません。忘れた場合は、そのままキーを再設定してください。', - 'config.webuiKey.newKey': '新しい WebUI キー', + 'config.webuiKey.newKey': '新しい管理キー', 'config.webuiKey.confirmKey': '新しいキーを確認', 'config.webuiKey.placeholder': '新しい平文キーを入力', 'config.webuiKey.confirmPlaceholder': '新しいキーをもう一度入力', 'config.webuiKey.generate': '安全なキーを生成', 'config.webuiKey.save': 'キーを保存', - 'config.webuiKey.open': 'WebUI を開く', - 'config.webuiKey.error.empty': 'WebUI キーを入力してください', - 'config.webuiKey.error.tooLong': 'WebUI キーは 512 文字以内で入力してください', - 'config.webuiKey.error.invalid': 'WebUI キーに制御文字は使用できません', - 'config.webuiKey.error.mismatch': '入力した WebUI キーが一致しません', - 'config.webuiKey.error.openFailed': 'WebUI を開けませんでした:{error}', - 'config.webuiKey.notice.updated': 'WebUI キーを更新しました', + 'config.webuiKey.open': 'Web 管理画面を開く', + 'config.webuiKey.error.empty': '管理キーを入力してください', + 'config.webuiKey.error.tooLong': '管理キーは 512 文字以内で入力してください', + 'config.webuiKey.error.invalid': '管理キーに制御文字は使用できません', + 'config.webuiKey.error.mismatch': '入力した 管理キーが一致しません', + 'config.webuiKey.error.openFailed': 'Web 管理画面を開けませんでした:{error}', + 'config.webuiKey.notice.updated': '管理キーを更新しました', 'config.keys.title': '認証キー', 'config.keys.count': 'キー数', 'config.keys.add': '認証キーを追加', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index dd3da98..a9c9bbf 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -973,23 +973,23 @@ export const en: Record = { 'config.diagnostics.error.nonNegativeInteger': 'Log size and error log retention must be non-negative integers', 'config.diagnostics.error.redisRetention': 'Usage queue retention must be an integer between 1 and 3600', 'config.diagnostics.error.openLogs': 'Failed to open the log folder: {{error}}', - 'config.webuiKey.title': 'WebUI Key', + 'config.webuiKey.title': 'Management Key', 'config.webuiKey.heading': 'Management access credential', - 'config.webuiKey.description': 'Used to sign in to the CPA WebUI and call the Management API. It is different from the API authentication keys above.', + 'config.webuiKey.description': 'Used to sign in to the CPA web management console and call the Management API. It is different from the API authentication keys above.', 'config.webuiKey.securityHint': 'The current key is not displayed. If you forget it, simply reset the key.', - 'config.webuiKey.newKey': 'New WebUI key', + 'config.webuiKey.newKey': 'New management key', 'config.webuiKey.confirmKey': 'Confirm new key', 'config.webuiKey.placeholder': 'Enter a new plaintext key', 'config.webuiKey.confirmPlaceholder': 'Enter the new key again', 'config.webuiKey.generate': 'Generate secure key', 'config.webuiKey.save': 'Save key', - 'config.webuiKey.open': 'Open WebUI', - 'config.webuiKey.error.empty': 'The WebUI key cannot be empty', - 'config.webuiKey.error.tooLong': 'The WebUI key cannot exceed 512 characters', - 'config.webuiKey.error.invalid': 'The WebUI key cannot contain control characters', - 'config.webuiKey.error.mismatch': 'The WebUI keys do not match', - 'config.webuiKey.error.openFailed': 'Could not open the WebUI: {error}', - 'config.webuiKey.notice.updated': 'WebUI key updated', + 'config.webuiKey.open': 'Open web management console', + 'config.webuiKey.error.empty': 'The management key cannot be empty', + 'config.webuiKey.error.tooLong': 'The management key cannot exceed 512 characters', + 'config.webuiKey.error.invalid': 'The management key cannot contain control characters', + 'config.webuiKey.error.mismatch': 'The management keys do not match', + 'config.webuiKey.error.openFailed': 'Could not open the web management console: {error}', + 'config.webuiKey.notice.updated': 'Management key updated', 'config.keys.title': 'Authentication Keys', 'config.keys.count': 'Key count', 'config.keys.add': 'Add Authentication Key', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index ef5eb34..d74ed72 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -972,23 +972,23 @@ export const zhCN = { 'config.diagnostics.error.nonNegativeInteger': '日志容量和错误日志保留数必须是非负整数', 'config.diagnostics.error.redisRetention': '用量队列保留时间必须是 1 到 3600 之间的整数', 'config.diagnostics.error.openLogs': '打开日志文件失败:{{error}}', - 'config.webuiKey.title': 'WebUI 密钥', + 'config.webuiKey.title': '管理密钥', 'config.webuiKey.heading': '管理接口访问凭据', - 'config.webuiKey.description': '用于登录 CPA WebUI 和调用管理接口,与上方 API 鉴权密钥用途不同。', + 'config.webuiKey.description': '用于登录 CPA 网页管理后台和调用管理接口,与上方 API 鉴权密钥用途不同。', 'config.webuiKey.securityHint': '现有密钥不会显示;如果忘记请直接重新设置密钥', - 'config.webuiKey.newKey': '新 WebUI 密钥', + 'config.webuiKey.newKey': '新管理密钥', 'config.webuiKey.confirmKey': '确认新密钥', 'config.webuiKey.placeholder': '输入新的明文密钥', 'config.webuiKey.confirmPlaceholder': '再次输入新密钥', 'config.webuiKey.generate': '生成安全密钥', 'config.webuiKey.save': '保存密钥', - 'config.webuiKey.open': '打开 WebUI', - 'config.webuiKey.error.empty': 'WebUI 密钥不能为空', - 'config.webuiKey.error.tooLong': 'WebUI 密钥不能超过 512 个字符', - 'config.webuiKey.error.invalid': 'WebUI 密钥不能包含控制字符', - 'config.webuiKey.error.mismatch': '两次输入的 WebUI 密钥不一致', - 'config.webuiKey.error.openFailed': '无法打开 WebUI:{error}', - 'config.webuiKey.notice.updated': 'WebUI 密钥已更新', + 'config.webuiKey.open': '打开网页管理后台', + 'config.webuiKey.error.empty': '管理密钥不能为空', + 'config.webuiKey.error.tooLong': '管理密钥不能超过 512 个字符', + 'config.webuiKey.error.invalid': '管理密钥不能包含控制字符', + 'config.webuiKey.error.mismatch': '两次输入的管理密钥不一致', + 'config.webuiKey.error.openFailed': '无法打开网页管理后台:{error}', + 'config.webuiKey.notice.updated': '管理密钥已更新', 'config.keys.title': '鉴权密钥', 'config.keys.count': '密钥数量', 'config.keys.add': '新增鉴权密钥',