Bump secrets-mcp to 0.3.8 (tag 0.3.7 already used). - Junction table entry_secrets; secrets user-scoped with type - Per-user unique secrets.name; link_secret_names on add - Manual migrations + migrate script; MCP/tool and Web updates Made-with: Cursor
105 lines
3.5 KiB
Rust
105 lines
3.5 KiB
Rust
use anyhow::Result;
|
|
use serde_json::Value;
|
|
use sqlx::PgPool;
|
|
use std::collections::HashMap;
|
|
use uuid::Uuid;
|
|
|
|
use crate::crypto;
|
|
use crate::service::search::{fetch_secrets_for_entries, resolve_entry, resolve_entry_by_id};
|
|
|
|
/// Decrypt a single named field from an entry.
|
|
/// `folder` is optional; if omitted and multiple entries share the name, an error is returned.
|
|
pub async fn get_secret_field(
|
|
pool: &PgPool,
|
|
name: &str,
|
|
folder: Option<&str>,
|
|
field_name: &str,
|
|
master_key: &[u8; 32],
|
|
user_id: Option<Uuid>,
|
|
) -> Result<Value> {
|
|
let entry = resolve_entry(pool, name, folder, user_id).await?;
|
|
|
|
let entry_ids = vec![entry.id];
|
|
let secrets_map = fetch_secrets_for_entries(pool, &entry_ids).await?;
|
|
let fields = secrets_map.get(&entry.id).map(Vec::as_slice).unwrap_or(&[]);
|
|
|
|
let field = fields
|
|
.iter()
|
|
.find(|f| f.name == field_name)
|
|
.ok_or_else(|| anyhow::anyhow!("Secret field '{}' not found", field_name))?;
|
|
|
|
crypto::decrypt_json(master_key, &field.encrypted)
|
|
}
|
|
|
|
/// Decrypt all secret fields from an entry. Returns a map field_name → decrypted Value.
|
|
/// `folder` is optional; if omitted and multiple entries share the name, an error is returned.
|
|
pub async fn get_all_secrets(
|
|
pool: &PgPool,
|
|
name: &str,
|
|
folder: Option<&str>,
|
|
master_key: &[u8; 32],
|
|
user_id: Option<Uuid>,
|
|
) -> Result<HashMap<String, Value>> {
|
|
let entry = resolve_entry(pool, name, folder, user_id).await?;
|
|
|
|
let entry_ids = vec![entry.id];
|
|
let secrets_map = fetch_secrets_for_entries(pool, &entry_ids).await?;
|
|
let fields = secrets_map.get(&entry.id).map(Vec::as_slice).unwrap_or(&[]);
|
|
|
|
let mut map = HashMap::new();
|
|
for f in fields {
|
|
let decrypted = crypto::decrypt_json(master_key, &f.encrypted)?;
|
|
map.insert(f.name.clone(), decrypted);
|
|
}
|
|
Ok(map)
|
|
}
|
|
|
|
/// Decrypt a single named field from an entry, located by its UUID.
|
|
pub async fn get_secret_field_by_id(
|
|
pool: &PgPool,
|
|
entry_id: Uuid,
|
|
field_name: &str,
|
|
master_key: &[u8; 32],
|
|
user_id: Option<Uuid>,
|
|
) -> Result<Value> {
|
|
resolve_entry_by_id(pool, entry_id, user_id)
|
|
.await
|
|
.map_err(|_| anyhow::anyhow!("Entry with id '{}' not found", entry_id))?;
|
|
|
|
let entry_ids = vec![entry_id];
|
|
let secrets_map = fetch_secrets_for_entries(pool, &entry_ids).await?;
|
|
let fields = secrets_map.get(&entry_id).map(Vec::as_slice).unwrap_or(&[]);
|
|
|
|
let field = fields
|
|
.iter()
|
|
.find(|f| f.name == field_name)
|
|
.ok_or_else(|| anyhow::anyhow!("Secret field '{}' not found", field_name))?;
|
|
|
|
crypto::decrypt_json(master_key, &field.encrypted)
|
|
}
|
|
|
|
/// Decrypt all secret fields from an entry, located by its UUID.
|
|
/// Returns a map field_name → decrypted Value.
|
|
pub async fn get_all_secrets_by_id(
|
|
pool: &PgPool,
|
|
entry_id: Uuid,
|
|
master_key: &[u8; 32],
|
|
user_id: Option<Uuid>,
|
|
) -> Result<HashMap<String, Value>> {
|
|
// Validate entry exists (and that it belongs to the requesting user)
|
|
resolve_entry_by_id(pool, entry_id, user_id)
|
|
.await
|
|
.map_err(|_| anyhow::anyhow!("Entry with id '{}' not found", entry_id))?;
|
|
|
|
let entry_ids = vec![entry_id];
|
|
let secrets_map = fetch_secrets_for_entries(pool, &entry_ids).await?;
|
|
let fields = secrets_map.get(&entry_id).map(Vec::as_slice).unwrap_or(&[]);
|
|
|
|
let mut map = HashMap::new();
|
|
for f in fields {
|
|
let decrypted = crypto::decrypt_json(master_key, &f.encrypted)?;
|
|
map.insert(f.name.clone(), decrypted);
|
|
}
|
|
Ok(map)
|
|
}
|