diff --git a/src/lib.rs b/src/lib.rs index d0da82c..7ce0e6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ // Modules that implement our hierarchical signing system pub mod vault_setup; +pub mod vault_init; pub mod document_service; pub mod api; // Re-export main components for easier access pub use vault_setup::VaultClient; +pub use vault_init::initialize_vault; pub use document_service::DocumentService; pub use api::start_api; diff --git a/src/main.rs b/src/main.rs index e4eeed6..2efb99b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,264 +1,10 @@ -use anyhow::{Context, Result}; -use reqwest::{Client, StatusCode}; -use serde::{Deserialize, Serialize}; -use std::{ - env, - fs::File, - io::Write, - path::Path, - time::Duration, -}; -use tokio::time::sleep; -use tracing::{info, warn, error, debug, instrument}; +use anyhow::Result; +use std::env; +use tracing::{info}; use tracing_subscriber::{fmt, EnvFilter}; // Import our library -use vault_hier::start_api; - -// Vault API response structures -#[derive(Debug, Deserialize)] -struct InitResponse { - keys: Vec, - keys_base64: Vec, - root_token: String, -} - -#[derive(Debug, Deserialize)] -struct SealStatusResponse { - sealed: bool, - t: u8, - n: u8, - progress: u8, -} - -#[derive(Debug, Serialize)] -struct InitRequest { - secret_shares: u8, - secret_threshold: u8, -} - -#[derive(Debug, Serialize)] -struct UnsealRequest { - key: String, -} - -// Function to save Vault credentials to a file -fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> { - // For JSON output - if file_path.ends_with(".json") { - let json = serde_json::json!({ - "keys": response.keys, - "keys_base64": response.keys_base64, - "root_token": response.root_token - }); - - let mut file = File::create(Path::new(file_path))?; - file.write_all(serde_json::to_string_pretty(&json)?.as_bytes())?; - info!("Credentials saved to JSON file: {}", file_path); - return Ok(()); - } - - // For plaintext output (legacy format) - let mut file = File::create(Path::new(file_path))?; - writeln!(file, "Unseal Keys:")?; - for (i, key) in response.keys.iter().enumerate() { - writeln!(file, "Key {}: {}", i + 1, key)?; - } - writeln!(file, "Base64 Unseal Keys:")?; - for (i, key) in response.keys_base64.iter().enumerate() { - writeln!(file, "Key {}: {}", i + 1, key)?; - } - writeln!(file)?; - writeln!(file, "Root Token: {}", response.root_token)?; - - info!("Credentials saved to {}", file_path); - Ok(()) -} - -// Wait for Vault to become available -#[instrument(skip(addr))] -async fn wait_for_vault(addr: &str) -> Result<()> { - info!("Waiting for Vault to be ready..."); - - let client = Client::new(); - - for i in 1..=30 { - let health_url = format!("{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true", addr); - - match client.get(&health_url).timeout(Duration::from_secs(1)).send().await { - Ok(response) => { - let status = response.status().as_u16(); - // Accept any of these status codes as "available" - if matches!(status, 200 | 429 | 472 | 473 | 501 | 503) { - info!("Vault is available! Status code: {}", status); - return Ok(()); - } - - debug!("Vault returned unexpected status code: {}", status); - }, - Err(e) => { - debug!("Error connecting to Vault: {}", e); - } - } - - if i == 30 { - error!("Timed out waiting for Vault to become available"); - return Err(anyhow::anyhow!("Timed out waiting for Vault to become available")); - } - - info!("Vault is unavailable - sleeping (attempt {}/30)", i); - sleep(Duration::from_secs(2)).await; - } - - Ok(()) -} - -#[instrument(skip(client, addr))] -async fn check_init_status(client: &Client, addr: &str) -> Result { - info!("Checking if Vault is already initialized..."); - - let response = client - .get(format!("{}/v1/sys/init", addr)) - .send() - .await?; - - if response.status().is_success() { - let status = response.json::().await?; - if let Some(initialized) = status.get("initialized").and_then(|v| v.as_bool()) { - return Ok(initialized); - } - } - - // If we couldn't determine, assume not initialized - Ok(false) -} - -#[instrument(skip(client, addr))] -async fn check_seal_status(client: &Client, addr: &str) -> Result { - info!("Checking Vault seal status..."); - - let response = client - .get(format!("{}/v1/sys/seal-status", addr)) - .send() - .await?; - - if response.status().is_success() { - let status = response.json::().await?; - info!("Seal status: sealed={}, threshold={}, shares={}, progress={}", - status.sealed, status.t, status.n, status.progress); - return Ok(status); - } else { - let error_text = response.text().await?; - error!("Failed to get seal status: {}", error_text); - anyhow::bail!("Failed to get seal status: {}", error_text); - } -} - -#[instrument(skip(client, addr))] -async fn init_vault(client: &Client, addr: &str) -> Result { - // First check if already initialized - let initialized = check_init_status(client, addr).await?; - - if initialized { - error!("Vault is already initialized. Cannot re-initialize."); - anyhow::bail!("Vault is already initialized. Cannot re-initialize."); - } - - info!("Initializing Vault..."); - - // Configure with 5 key shares and a threshold of 3 - // This is a standard production configuration, requiring 3 out of 5 keys to unseal - let init_req = InitRequest { - secret_shares: 5, - secret_threshold: 3, - }; - - let response = client - .put(format!("{}/v1/sys/init", addr)) - .json(&init_req) - .send() - .await?; - - match response.status() { - StatusCode::OK => { - let init_response = response.json::().await?; - info!("Vault initialized successfully!"); - Ok(init_response) - } - status => { - let error_text = response.text().await?; - error!("Failed to initialize Vault: {} - {}", status, error_text); - anyhow::bail!("Failed to initialize Vault: {} - {}", status, error_text); - } - } -} - -#[instrument(skip(client, addr, unseal_keys))] -async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Result<()> { - // First check the current seal status - let mut seal_status = check_seal_status(client, addr).await?; - - if !seal_status.sealed { - info!("Vault is already unsealed!"); - return Ok(()); - } - - info!("Unsealing Vault..."); - - // We need to provide enough keys to meet the threshold - // The threshold is in seal_status.t - let required_keys = seal_status.t as usize; - - if unseal_keys.len() < required_keys { - error!( - "Not enough unseal keys provided. Need {} keys, but only have {}", - required_keys, - unseal_keys.len() - ); - anyhow::bail!( - "Not enough unseal keys provided. Need {} keys, but only have {}", - required_keys, - unseal_keys.len() - ); - } - - // Apply each key one at a time until unsealed - for (i, key) in unseal_keys.iter().take(required_keys).enumerate() { - info!("Applying unseal key {}/{}...", i + 1, required_keys); - - let unseal_req = UnsealRequest { - key: key.clone(), - }; - - let response = client - .put(format!("{}/v1/sys/unseal", addr)) - .json(&unseal_req) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - error!("Failed to apply unseal key: {}", error_text); - anyhow::bail!("Failed to apply unseal key: {}", error_text); - } - - // Check the updated seal status - seal_status = check_seal_status(client, addr).await?; - - if !seal_status.sealed { - info!("Vault unsealed successfully after applying {} keys!", i + 1); - return Ok(()); - } - } - - // If we get here, we've applied all keys but Vault is still sealed - if seal_status.sealed { - error!("Applied all available unseal keys, but Vault is still sealed"); - anyhow::bail!("Applied all available unseal keys, but Vault is still sealed"); - } - - Ok(()) -} +use vault_hier::{start_api, initialize_vault}; #[tokio::main] async fn main() -> Result<()> { @@ -270,7 +16,6 @@ async fn main() -> Result<()> { // Get Vault address from env var or use default let vault_addr = env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()); - let client = Client::new(); // Get API port from env var or use default let api_port = env::var("API_PORT") @@ -281,163 +26,9 @@ async fn main() -> Result<()> { info!("Vault address: {}", vault_addr); info!("Connecting to Vault at: {}", vault_addr); - // Wait for Vault to be available - wait_for_vault(&vault_addr).await?; + // Initialize and unseal Vault, get the root token + let root_token = initialize_vault(&vault_addr).await?; - // Get Vault status to display - let health_url = format!("{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true", vault_addr); - match client.get(&health_url).send().await { - Ok(response) => { - if response.status().is_success() { - let status_text = response.text().await?; - info!("Vault status: {}", status_text); - } - }, - Err(e) => warn!("Error getting Vault status: {}", e), - } - - // First check if Vault is already initialized - let initialized = check_init_status(&client, &vault_addr).await?; - let mut root_token = String::new(); - - if initialized { - info!("Vault is already initialized."); - - // Check if Vault is sealed - let seal_status = check_seal_status(&client, &vault_addr).await?; - - if seal_status.sealed { - info!("Vault is sealed. Looking for unseal keys..."); - - // Try to load unseal keys from environment variables - let mut unseal_keys = Vec::new(); - for i in 1..=5 { - match env::var(format!("VAULT_UNSEAL_KEY_{}", i)) { - Ok(key) => { - info!("Found unseal key {} from environment", i); - unseal_keys.push(key); - }, - Err(_) => { - debug!("Unseal key {} not found in environment", i); - } - } - } - - // If we have unseal keys, try to unseal - if !unseal_keys.is_empty() { - info!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len()); - unseal_vault(&client, &vault_addr, &unseal_keys).await?; - } else { - warn!("No unseal keys found. Vault remains sealed."); - info!("To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables."); - } - } else { - info!("Vault is already unsealed."); - } - - // Try to load root token from environment or credentials file - match env::var("VAULT_TOKEN") { - Ok(token) => { - info!("Found root token from environment"); - root_token = token; - }, - Err(_) => { - // Try to load from credentials file - if let Ok(contents) = std::fs::read_to_string("vault-credentials.json") { - if let Ok(creds) = serde_json::from_str::(&contents) { - if let Some(token) = creds["root_token"].as_str() { - info!("Found root token from credentials file"); - root_token = token.to_string(); - } - } - } - } - } - - if root_token.is_empty() { - error!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file."); - anyhow::bail!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file."); - } - } else { - // Initialize Vault - info!("Vault is not initialized. Proceeding with initialization..."); - let init_response = init_vault(&client, &vault_addr).await?; - - // Save credentials to files - info!("Saving credentials to files..."); - let current_dir = std::env::current_dir().context("Failed to get current directory")?; - - // Save as JSON (new format) - let json_path = current_dir.join("vault-credentials.json"); - save_credentials(&init_response, json_path.to_str().unwrap())?; - info!("JSON credentials saved to: {}", json_path.display()); - - // Save as text (for backward compatibility) - let text_path = current_dir.join("vault-credentials.txt"); - save_credentials(&init_response, text_path.to_str().unwrap())?; - info!("Text credentials saved to: {}", text_path.display()); - - // Also save to /app/data as a backup for Docker volume mounting - if let Ok(()) = std::fs::create_dir_all("/app/data") { - let docker_json_path = "/app/data/vault-credentials.json"; - save_credentials(&init_response, docker_json_path)?; - info!("Backup JSON credentials saved to Docker volume at: {}", docker_json_path); - - let docker_text_path = "/app/data/vault-credentials.txt"; - save_credentials(&init_response, docker_text_path)?; - info!("Backup text credentials saved to Docker volume at: {}", docker_text_path); - } - - info!("========================================="); - info!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY"); - info!("========================================="); - info!("Root Token: {}", init_response.root_token); - info!("Unseal Keys (first 3 of 5 needed to unseal):"); - for (i, key) in init_response.keys_base64.iter().enumerate() { - info!("Key {}: {}", i + 1, key); - } - info!("========================================="); - - // Unseal Vault using the first three keys - let unseal_keys = init_response.keys_base64.iter() - .take(3) // We only need threshold number of keys (3) - .cloned() - .collect::>(); - - unseal_vault(&client, &vault_addr, &unseal_keys).await?; - - info!("Vault initialization and unseal complete!"); - - // Set root token - root_token = init_response.root_token; - } - - // Look for any existing credentials and copy them to the mounted volume - if let Ok(metadata) = std::fs::metadata("vault-credentials.json") { - if metadata.is_file() { - info!("Found JSON credentials file, ensuring it's saved to Docker volume..."); - if let Ok(()) = std::fs::create_dir_all("/app/data") { - match std::fs::copy("vault-credentials.json", "/app/data/vault-credentials.json") { - Ok(_) => info!("JSON credentials saved to Docker volume"), - Err(e) => warn!("Failed to copy JSON credentials: {}", e), - } - } - } - } - - if let Ok(metadata) = std::fs::metadata("vault-credentials.txt") { - if metadata.is_file() { - info!("Found text credentials file, ensuring it's saved to Docker volume..."); - if let Ok(()) = std::fs::create_dir_all("/app/data") { - match std::fs::copy("vault-credentials.txt", "/app/data/vault-credentials.txt") { - Ok(_) => info!("Text credentials saved to Docker volume"), - Err(e) => warn!("Failed to copy text credentials: {}", e), - } - } - } - } - - info!("Vault setup complete!"); info!("Starting hierarchical document signing API..."); // Start the hierarchical signing API diff --git a/src/vault_init.rs b/src/vault_init.rs new file mode 100644 index 0000000..9d489e1 --- /dev/null +++ b/src/vault_init.rs @@ -0,0 +1,173 @@ +use anyhow::{Context, Result}; +use reqwest::Client; +use std::{ + env, + fs, +}; +use tracing::{info, warn, error, debug}; + +use crate::vault_setup::VaultClient; + +/// Initialize and unseal the Vault, returning the root token for further operations +pub async fn initialize_vault(vault_addr: &str) -> Result { + let client = Client::new(); + + // Wait for Vault to be available + VaultClient::wait_for_vault(vault_addr).await?; + + // Display Vault health status + let health_url = format!("{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true", vault_addr); + match client.get(&health_url).send().await { + Ok(response) => { + if response.status().is_success() { + let status_text = response.text().await?; + info!("Vault status: {}", status_text); + } + }, + Err(e) => warn!("Error getting Vault status: {}", e), + } + + // First check if Vault is already initialized + let initialized = VaultClient::check_init_status(&client, vault_addr).await?; + let mut root_token = String::new(); + + if initialized { + info!("Vault is already initialized."); + + // Check if Vault is sealed + let seal_status = VaultClient::check_seal_status(&client, vault_addr).await?; + + if seal_status.sealed { + info!("Vault is sealed. Looking for unseal keys..."); + + // Try to load unseal keys from environment variables + let mut unseal_keys = Vec::new(); + for i in 1..=5 { + match env::var(format!("VAULT_UNSEAL_KEY_{}", i)) { + Ok(key) => { + info!("Found unseal key {} from environment", i); + unseal_keys.push(key); + }, + Err(_) => { + debug!("Unseal key {} not found in environment", i); + } + } + } + + // If we have unseal keys, try to unseal + if !unseal_keys.is_empty() { + info!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len()); + VaultClient::unseal_vault(&client, vault_addr, &unseal_keys).await?; + } else { + warn!("No unseal keys found. Vault remains sealed."); + info!("To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables."); + } + } else { + info!("Vault is already unsealed."); + } + + // Try to load root token from environment or credentials file + match env::var("VAULT_TOKEN") { + Ok(token) => { + info!("Found root token from environment"); + root_token = token; + }, + Err(_) => { + // Try to load from credentials file + if let Ok(contents) = fs::read_to_string("vault-credentials.json") { + if let Ok(creds) = serde_json::from_str::(&contents) { + if let Some(token) = creds["root_token"].as_str() { + info!("Found root token from credentials file"); + root_token = token.to_string(); + } + } + } + } + } + + if root_token.is_empty() { + error!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file."); + anyhow::bail!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file."); + } + } else { + // Initialize Vault + info!("Vault is not initialized. Proceeding with initialization..."); + let init_response = VaultClient::init_vault(&client, vault_addr).await?; + + // Save credentials to files + info!("Saving credentials to files..."); + let current_dir = std::env::current_dir().context("Failed to get current directory")?; + + // Save as JSON (new format) + let json_path = current_dir.join("vault-credentials.json"); + VaultClient::save_credentials(&init_response, json_path.to_str().unwrap())?; + info!("JSON credentials saved to: {}", json_path.display()); + + // Save as text (for backward compatibility) + let text_path = current_dir.join("vault-credentials.txt"); + VaultClient::save_credentials(&init_response, text_path.to_str().unwrap())?; + info!("Text credentials saved to: {}", text_path.display()); + + // Also save to /app/data as a backup for Docker volume mounting + if let Ok(()) = std::fs::create_dir_all("/app/data") { + let docker_json_path = "/app/data/vault-credentials.json"; + VaultClient::save_credentials(&init_response, docker_json_path)?; + info!("Backup JSON credentials saved to Docker volume at: {}", docker_json_path); + + let docker_text_path = "/app/data/vault-credentials.txt"; + VaultClient::save_credentials(&init_response, docker_text_path)?; + info!("Backup text credentials saved to Docker volume at: {}", docker_text_path); + } + + info!("========================================="); + info!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY"); + info!("========================================="); + info!("Root Token: {}", init_response.root_token); + info!("Unseal Keys (first 3 of 5 needed to unseal):"); + for (i, key) in init_response.keys_base64.iter().enumerate() { + info!("Key {}: {}", i + 1, key); + } + info!("========================================="); + + // Unseal Vault using the first three keys + let unseal_keys = init_response.keys_base64.iter() + .take(3) // We only need threshold number of keys (3) + .cloned() + .collect::>(); + + VaultClient::unseal_vault(&client, vault_addr, &unseal_keys).await?; + + info!("Vault initialization and unseal complete!"); + + // Set root token + root_token = init_response.root_token; + } + + // Look for any existing credentials and copy them to the mounted volume + if let Ok(metadata) = std::fs::metadata("vault-credentials.json") { + if metadata.is_file() { + info!("Found JSON credentials file, ensuring it's saved to Docker volume..."); + if let Ok(()) = std::fs::create_dir_all("/app/data") { + match std::fs::copy("vault-credentials.json", "/app/data/vault-credentials.json") { + Ok(_) => info!("JSON credentials saved to Docker volume"), + Err(e) => warn!("Failed to copy JSON credentials: {}", e), + } + } + } + } + + if let Ok(metadata) = std::fs::metadata("vault-credentials.txt") { + if metadata.is_file() { + info!("Found text credentials file, ensuring it's saved to Docker volume..."); + if let Ok(()) = std::fs::create_dir_all("/app/data") { + match std::fs::copy("vault-credentials.txt", "/app/data/vault-credentials.txt") { + Ok(_) => info!("Text credentials saved to Docker volume"), + Err(e) => warn!("Failed to copy text credentials: {}", e), + } + } + } + } + + info!("Vault setup complete!"); + Ok(root_token) +} diff --git a/src/vault_setup.rs b/src/vault_setup.rs index c603d31..771b3cf 100644 --- a/src/vault_setup.rs +++ b/src/vault_setup.rs @@ -2,8 +2,42 @@ use anyhow::{Context, Result}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::json; +use std::{ + fs::File, + io::Write, + path::Path, + time::Duration, +}; +use tokio::time::sleep; use tracing::{info, error, debug, instrument}; +// Vault API response structures +#[derive(Debug, Deserialize)] +pub struct InitResponse { + pub keys: Vec, + pub keys_base64: Vec, + pub root_token: String, +} + +#[derive(Debug, Deserialize)] +pub struct SealStatusResponse { + pub sealed: bool, + pub t: u8, + pub n: u8, + pub progress: u8, +} + +#[derive(Debug, Serialize)] +pub struct InitRequest { + pub secret_shares: u8, + pub secret_threshold: u8, +} + +#[derive(Debug, Serialize)] +pub struct UnsealRequest { + pub key: String, +} + // Department types for organizational structure #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Department { @@ -35,6 +69,228 @@ impl VaultClient { } } + // Wait for Vault to become available + #[instrument(skip(addr))] + pub async fn wait_for_vault(addr: &str) -> Result<()> { + info!("Waiting for Vault to be ready..."); + + let client = Client::new(); + + for i in 1..=30 { + let health_url = format!("{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true", addr); + + match client.get(&health_url).timeout(Duration::from_secs(1)).send().await { + Ok(response) => { + let status = response.status().as_u16(); + // Accept any of these status codes as "available" + if matches!(status, 200 | 429 | 472 | 473 | 501 | 503) { + info!("Vault is available! Status code: {}", status); + return Ok(()); + } + + debug!("Vault returned unexpected status code: {}", status); + }, + Err(e) => { + debug!("Error connecting to Vault: {}", e); + } + } + + if i == 30 { + error!("Timed out waiting for Vault to become available"); + return Err(anyhow::anyhow!("Timed out waiting for Vault to become available")); + } + + info!("Vault is unavailable - sleeping (attempt {}/30)", i); + sleep(Duration::from_secs(2)).await; + } + + Ok(()) + } + + // Function to check if Vault is initialized + #[instrument(skip(client, addr))] + pub async fn check_init_status(client: &Client, addr: &str) -> Result { + info!("Checking if Vault is already initialized..."); + + let response = client + .get(format!("{}/v1/sys/init", addr)) + .send() + .await?; + + if response.status().is_success() { + let status = response.json::().await?; + if let Some(initialized) = status.get("initialized").and_then(|v| v.as_bool()) { + return Ok(initialized); + } + } + + // If we couldn't determine, assume not initialized + Ok(false) + } + + // Function to check Vault seal status + #[instrument(skip(client, addr))] + pub async fn check_seal_status(client: &Client, addr: &str) -> Result { + info!("Checking Vault seal status..."); + + let response = client + .get(format!("{}/v1/sys/seal-status", addr)) + .send() + .await?; + + if response.status().is_success() { + let status = response.json::().await?; + info!("Seal status: sealed={}, threshold={}, shares={}, progress={}", + status.sealed, status.t, status.n, status.progress); + return Ok(status); + } else { + let error_text = response.text().await?; + error!("Failed to get seal status: {}", error_text); + anyhow::bail!("Failed to get seal status: {}", error_text); + } + } + + // Function to initialize Vault + #[instrument(skip(client, addr))] + pub async fn init_vault(client: &Client, addr: &str) -> Result { + // First check if already initialized + let initialized = Self::check_init_status(client, addr).await?; + + if initialized { + error!("Vault is already initialized. Cannot re-initialize."); + anyhow::bail!("Vault is already initialized. Cannot re-initialize."); + } + + info!("Initializing Vault..."); + + // Configure with 5 key shares and a threshold of 3 + // This is a standard production configuration, requiring 3 out of 5 keys to unseal + let init_req = InitRequest { + secret_shares: 5, + secret_threshold: 3, + }; + + let response = client + .put(format!("{}/v1/sys/init", addr)) + .json(&init_req) + .send() + .await?; + + match response.status() { + StatusCode::OK => { + let init_response = response.json::().await?; + info!("Vault initialized successfully!"); + Ok(init_response) + } + status => { + let error_text = response.text().await?; + error!("Failed to initialize Vault: {} - {}", status, error_text); + anyhow::bail!("Failed to initialize Vault: {} - {}", status, error_text); + } + } + } + + // Function to unseal Vault + #[instrument(skip(client, addr, unseal_keys))] + pub async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Result<()> { + // First check the current seal status + let mut seal_status = Self::check_seal_status(client, addr).await?; + + if !seal_status.sealed { + info!("Vault is already unsealed!"); + return Ok(()); + } + + info!("Unsealing Vault..."); + + // We need to provide enough keys to meet the threshold + // The threshold is in seal_status.t + let required_keys = seal_status.t as usize; + + if unseal_keys.len() < required_keys { + error!( + "Not enough unseal keys provided. Need {} keys, but only have {}", + required_keys, + unseal_keys.len() + ); + anyhow::bail!( + "Not enough unseal keys provided. Need {} keys, but only have {}", + required_keys, + unseal_keys.len() + ); + } + + // Apply each key one at a time until unsealed + for (i, key) in unseal_keys.iter().take(required_keys).enumerate() { + info!("Applying unseal key {}/{}...", i + 1, required_keys); + + let unseal_req = UnsealRequest { + key: key.clone(), + }; + + let response = client + .put(format!("{}/v1/sys/unseal", addr)) + .json(&unseal_req) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + error!("Failed to apply unseal key: {}", error_text); + anyhow::bail!("Failed to apply unseal key: {}", error_text); + } + + // Check the updated seal status + seal_status = Self::check_seal_status(client, addr).await?; + + if !seal_status.sealed { + info!("Vault unsealed successfully after applying {} keys!", i + 1); + return Ok(()); + } + } + + // If we get here, we've applied all keys but Vault is still sealed + if seal_status.sealed { + error!("Applied all available unseal keys, but Vault is still sealed"); + anyhow::bail!("Applied all available unseal keys, but Vault is still sealed"); + } + + Ok(()) + } + + // Function to save Vault credentials to a file + pub fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> { + // For JSON output + if file_path.ends_with(".json") { + let json = serde_json::json!({ + "keys": response.keys, + "keys_base64": response.keys_base64, + "root_token": response.root_token + }); + + let mut file = File::create(Path::new(file_path))?; + file.write_all(serde_json::to_string_pretty(&json)?.as_bytes())?; + info!("Credentials saved to JSON file: {}", file_path); + return Ok(()); + } + + // For plaintext output (legacy format) + let mut file = File::create(Path::new(file_path))?; + writeln!(file, "Unseal Keys:")?; + for (i, key) in response.keys.iter().enumerate() { + writeln!(file, "Key {}: {}", i + 1, key)?; + } + writeln!(file, "Base64 Unseal Keys:")?; + for (i, key) in response.keys_base64.iter().enumerate() { + writeln!(file, "Key {}: {}", i + 1, key)?; + } + writeln!(file)?; + writeln!(file, "Root Token: {}", response.root_token)?; + + info!("Credentials saved to {}", file_path); + Ok(()) + } + // Enable required secrets engines #[instrument(skip(self))] pub async fn setup_secrets_engines(&self) -> Result<()> {