Remedial measures to enhance the detection of blank key situations

This commit is contained in:
lzt404 committed 2026-10-06 13:41:35 +08:00
1 parent b48128a81a
commit 8f23fac80b
6 files changed
+92 -46

No files matched your search

+25 -11
View File
@@ -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<String, String> {
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<bool, String> {
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<String, String> {
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<GuiConfigFile, String> {
)
};
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<GuiConfigFile, String> {
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)?;
+4 -4
View File
@@ -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}");
}
+33 -1
View File
@@ -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 {
+10 -10
View File
@@ -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': '認証キーを追加',
+10 -10
View File
@@ -973,23 +973,23 @@ export const en: Record<MessageKey, string> = {
'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',
+10 -10
View File
@@ -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': '新增鉴权密钥',