From 317c6df98f451a06e6a651d0376ee6649be43b32 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Tue, 6 Oct 2026 13:20:03 +0800 Subject: [PATCH] fix(core): prevent duplicate management mapping during startup - Preserve hashed management keys written by the v8 core - Avoid rewriting hashed keys with the GUI plaintext value - Prevent duplicate top-level management mappings Fixes #351 --- src-tauri/src/core_config/settings.rs | 30 +++++++++++++++++----- src-tauri/src/tests/core_config.rs | 37 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/core_config/settings.rs b/src-tauri/src/core_config/settings.rs index 31122eb..6535a49 100644 --- a/src-tauri/src/core_config/settings.rs +++ b/src-tauri/src/core_config/settings.rs @@ -621,12 +621,30 @@ pub(crate) fn apply_gui_managed_settings( &["observability", "logs", "request-log"], serde_norway::Value::Bool(config.request_log), )?; - changed |= set_core_yaml_schema_value( - document, - &["remote-management", "secret-key"], - &["management", "secret-key"], - serde_norway::Value::String(config.management_secret_key.clone()), - )?; + // v8 kernels persist this credential as a bcrypt/argon2 hash after a + // successful start. It is already the canonical value the kernel + // expects; replacing it with the GUI plaintext needlessly enters the + // format-preserving YAML editor and can produce a duplicate top-level + // `management` mapping for some valid config layouts. + let stored_management_key = document + .as_mapping() + .and_then(|root| nested_yaml_value(root, &["management", "secret-key"])) + .or_else(|| { + document.as_mapping().and_then(|root| { + nested_yaml_value(root, &["remote-management", "secret-key"]) + }) + }); + if !stored_management_key + .and_then(serde_norway::Value::as_str) + .is_some_and(is_hashed_management_secret_key) + { + changed |= set_core_yaml_schema_value( + document, + &["remote-management", "secret-key"], + &["management", "secret-key"], + serde_norway::Value::String(config.management_secret_key.clone()), + )?; + } changed |= set_core_yaml_nested_value( document, "plugins", diff --git a/src-tauri/src/tests/core_config.rs b/src-tauri/src/tests/core_config.rs index e1f077c..b24684b 100644 --- a/src-tauri/src/tests/core_config.rs +++ b/src-tauri/src/tests/core_config.rs @@ -1556,6 +1556,43 @@ fn v8_core_config_reads_and_writes_canonical_nested_fields() { } } +#[test] +fn v8_inline_management_hash_is_preserved_without_duplicate_section() { + let input = "config-version: 8\nserver: {host: 127.0.0.1, port: 8317}\nmanagement: {secret-key: $2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu}\naccess: {api-keys: [client-key]}\nrequests: {proxy-url: direct}\n"; + let mut config = GuiConfigFile::default(); + config.management_secret_key = "plaintext-management-key".into(); + let updated = apply_gui_managed_settings(input, &config) + .unwrap_or_else(|error| panic!("management key update failed: {error}")); + let parsed = serde_norway::from_str::(&updated).unwrap(); + assert_eq!(parsed["management"]["secret-key"], "$2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu"); +} + +#[test] +fn v8_multiline_management_hash_with_comments_is_preserved() { + let input = "# generated by CLIProxyAPI\n# keep this comment\nconfig-version: 8\nserver:\n host: 127.0.0.1\n port: 8317\nmanagement:\n secret-key: $2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu\n allow-remote: false\naccess:\n api-keys:\n - client-key\nrequests:\n proxy-url: direct\n"; + let mut config = GuiConfigFile::default(); + config.management_secret_key = "plaintext-management-key".into(); + let updated = apply_gui_managed_settings(input, &config) + .unwrap_or_else(|error| panic!("management key update failed: {error}")); + let parsed = serde_norway::from_str::(&updated).unwrap(); + assert_eq!(parsed["management"]["secret-key"], "$2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu"); + assert_eq!(parsed["management"]["allow-remote"], false); +} + +#[test] +fn gui_start_preserves_hashed_v8_management_key() { + let hash = "$2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu"; + let input = format!( + "config-version: 8\nserver: {{host: 127.0.0.1, port: 8317}}\nmanagement: {{secret-key: {hash}}}\naccess: {{api-keys: [client-key]}}\nrequests: {{proxy-url: direct}}\n" + ); + let mut config = GuiConfigFile::default(); + config.management_secret_key = "plaintext-management-key".into(); + let updated = apply_gui_managed_settings(&input, &config).unwrap(); + let parsed = serde_norway::from_str::(&updated).unwrap(); + assert_eq!(parsed["management"]["secret-key"], hash); + assert!(!updated.contains("plaintext-management-key")); +} + #[test] fn v8_omitted_fields_use_runtime_defaults_instead_of_gui_presets() { let settings = core_config_settings_from_value(