// file: crates/ksp-app-wallet-desk/src/wallet_secrets.rs // version: 1 //! Config-owned Wallet password candidate discovery for explicit unlock operations. const WALLET_SECRET_PREFIX: &str = "KSP_SECRET_WALLET_PASS_"; /// One Config-owned secret candidate retained only inside Rust. pub(crate) struct WalletSecretCandidate { variable_name: String, } impl WalletSecretCandidate { /// Explicitly reveals this candidate through Config's privileged management boundary. pub(crate) fn reveal(self, management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result> { return management.reveal_effective_environment_value(self.variable_name.as_str()); } } /// Discovers configured Wallet password candidates without revealing values. pub(crate) fn discover_wallet_secret_candidates( management: &ksp_config_lib::ConfigManagement, wallet_id: &str, ) -> ksp_core_lib::Result> { let reports = management.environment_report(); let reports = match reports { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut names = reports .into_iter() .filter(|report| { return report.sensitivity() == ksp_config_lib::ConfigSensitivity::Secret && report.effective_source().is_some() && report.variable_name().starts_with(WALLET_SECRET_PREFIX) && report.variable_name().len() > WALLET_SECRET_PREFIX.len(); }) .map(|report| return report.variable_name().to_owned()) .collect::>(); order_wallet_secret_candidate_names(wallet_id, names.as_mut_slice()); return std::result::Result::Ok(names.into_iter().map(|variable_name| return WalletSecretCandidate { variable_name }).collect()); } /// Returns the number of effective configured Wallet password candidates without revealing names or values. pub(crate) fn wallet_secret_candidate_count(management: &ksp_config_lib::ConfigManagement, wallet_id: &str) -> ksp_core_lib::Result { let candidates = discover_wallet_secret_candidates(management, wallet_id); return candidates.map(|values| return values.len()); } /// Normalizes a native Wallet filename to the deterministic Config secret label candidate. #[must_use] fn normalize_wallet_secret_label(wallet_id: &str) -> String { let stem = wallet_id.strip_suffix(crate::WALLET_FILE_SUFFIX).unwrap_or(wallet_id); let mut output = String::new(); let mut previous_separator = false; for byte in stem.bytes() { let normalized = if byte.is_ascii_alphanumeric() { byte.to_ascii_uppercase() as char } else { '_' }; if normalized == '_' { if output.is_empty() || previous_separator { continue; } output.push('_'); previous_separator = true; } else { output.push(normalized); previous_separator = false; } } while output.ends_with('_') { output.pop(); } return output; } fn order_wallet_secret_candidate_names(wallet_id: &str, names: &mut [String]) { let normalized = normalize_wallet_secret_label(wallet_id); names.sort_by(|left, right| { return wallet_secret_sort_key(left.as_str(), normalized.as_str()).cmp(&wallet_secret_sort_key(right.as_str(), normalized.as_str())); }); } fn wallet_secret_sort_key(variable_name: &str, normalized_filename: &str) -> (u8, u64, String) { let suffix = variable_name.strip_prefix(WALLET_SECRET_PREFIX).unwrap_or(variable_name); if suffix == normalized_filename && !normalized_filename.is_empty() { return (0, 0, suffix.to_owned()); } if !suffix.is_empty() && suffix.bytes().all(|byte| return byte.is_ascii_digit()) { let parsed = suffix.parse::().unwrap_or(u64::MAX); return (1, parsed, suffix.to_owned()); } return (2, 0, suffix.to_owned()); } #[cfg(test)] #[path = "../unit_tests/wallet_secrets.rs"] mod tests;