Remedial measures to enhance the detection of blank key situations
This commit is contained in:
1 parent
b48128a81a
commit
8f23fac80b
6 files changed
+92
-46
No files matched your search
@@ -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)?;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user