diff --git a/crates/colibri-vault/src/lib.rs b/crates/colibri-vault/src/lib.rs index 65b9885..17c6a96 100644 --- a/crates/colibri-vault/src/lib.rs +++ b/crates/colibri-vault/src/lib.rs @@ -93,13 +93,16 @@ pub async fn provision( for item in &items { if let Some(login) = &item.login { - if let Some(username) = &login.username { - if let Some(password) = &login.password { - // Username = KEY, Password = VALUE (Vaultwarden login item convention) - let key = username.trim().to_uppercase().replace([' ', '-'], "_"); - let val = password.trim(); - env_content.push_str(&format!("{key}={val}\n")); + // item.name = KEY, login.password = VALUE (Vaultwarden login item convention) + let raw_key = item.name.trim(); + if let Some(password) = &login.password { + let key = validate_key(raw_key); + if key.is_empty() { + tracing::warn!(item = item.name, "skipping item with invalid env key"); + continue; } + let val = password.trim(); + env_content.push_str(&format!("{key}={val}\n")); } } // Also handle secure notes (KEY=VALUE pairs separated by newlines) @@ -247,4 +250,45 @@ mod tests { let e = VaultError::CollectionNotFound("missing".into()); assert!(e.to_string().contains("missing")); } + + #[test] + fn key_from_item_name_not_username() { + // The contract: item.name = KEY, login.password = VALUE + // Not username=KEY (bug that would produce wrong .env output) + let item = SerdeItem { + id: "test-id".into(), + name: "OPENROUTER_API_KEY".into(), + kind: 1, + login: Some(SerdeLogin { + username: Some("irrelevant@email.com".into()), + password: Some("sk-or-v1-abc123".into()), + }), + notes: None, + }; + assert_eq!(item.name, "OPENROUTER_API_KEY"); + assert_eq!(item.login.as_ref().unwrap().password.as_deref(), Some("sk-or-v1-abc123")); + } + + #[test] + fn validate_key_rejects_dangerous_chars() { + assert_eq!(validate_key("MY_KEY"), "MY_KEY"); + assert_eq!(validate_key("my-key"), "MY_KEY"); + assert_eq!(validate_key("OpenRouter API Key"), "OPENROUTER_API_KEY"); + assert_eq!(validate_key("my.key.name"), "MY_KEY_NAME"); + // Reject injection attempts + assert!(validate_key("BAD;rm -rf /").is_empty()); + assert!(validate_key("KEY\nMALICIOUS=1").is_empty()); + assert!(validate_key("").is_empty()); + } +} + +/// Validate and normalize a key for .env output. +/// Reject keys with non-[A-Z0-9_] characters (matching the clawdie-vault-fetch helper). +fn validate_key(raw: &str) -> String { + let key = raw.to_uppercase().replace([' ', '-', '.'], "_"); + if key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !key.is_empty() { + key + } else { + String::new() + } }