use cm_domain::WorkspaceId; use sqlx::PgPool; use uuid::Uuid; use crate::crypto::{FileKey, Sealed}; use crate::BrokerError; /// Encrypted persistence for the `secrets` table. Plaintext exists only /// transiently inside broker memory during a capability call. pub struct SecretStore<'a> { pub pool: &'a PgPool, pub key: &'a FileKey, } impl SecretStore<'_> { pub async fn store( &self, workspace_id: WorkspaceId, kind: &str, plaintext: &str, ) -> Result { let sealed = self.key.seal(plaintext.as_bytes())?; let id = Uuid::now_v7(); sqlx::query!( "INSERT INTO secrets (id, workspace_id, kind, ciphertext, nonce) VALUES ($1, $2, $3, $4, $5)", id, workspace_id.as_uuid(), kind, sealed.ciphertext, sealed.nonce, ) .execute(self.pool) .await .map_err(|e| BrokerError::Io(e.to_string()))?; Ok(id) } pub async fn kind(&self, id: Uuid) -> Result { sqlx::query_scalar!("SELECT kind FROM secrets WHERE id = $1", id) .fetch_optional(self.pool) .await .map_err(|e| BrokerError::Io(e.to_string()))? .ok_or(BrokerError::NotFound) } /// Decrypts a secret for immediate use inside the broker. Callers must /// never serialize the result onto the wire. pub async fn reveal_internal(&self, id: Uuid) -> Result { let row = sqlx::query!("SELECT ciphertext, nonce FROM secrets WHERE id = $1", id) .fetch_optional(self.pool) .await .map_err(|e| BrokerError::Io(e.to_string()))? .ok_or(BrokerError::NotFound)?; let plaintext = self.key.open(&Sealed { ciphertext: row.ciphertext, nonce: row.nonce, })?; String::from_utf8(plaintext).map_err(|e| BrokerError::Crypto(e.to_string())) } }