agentic_connect/engine/
vault.rs1use ring::aead;
4use ring::rand::{SecureRandom, SystemRandom};
5use std::collections::HashMap;
6
7use crate::types::{ConnectError, ConnectResult, StoredCredential};
8
9pub struct CredentialVault {
11 credentials: HashMap<String, StoredCredential>,
12 encryption_key: Option<aead::LessSafeKey>,
13}
14
15impl CredentialVault {
16 pub fn new() -> Self {
18 Self {
19 credentials: HashMap::new(),
20 encryption_key: None,
21 }
22 }
23
24 pub fn with_encryption(passphrase: &str) -> ConnectResult<Self> {
26 let key = derive_key(passphrase)?;
27 Ok(Self {
28 credentials: HashMap::new(),
29 encryption_key: Some(key),
30 })
31 }
32
33 pub fn store(&mut self, cred: StoredCredential) {
35 self.credentials.insert(cred.name.clone(), cred);
36 }
37
38 pub fn retrieve(&self, name: &str) -> Option<&StoredCredential> {
40 self.credentials.get(name)
41 }
42
43 pub fn delete(&mut self, name: &str) -> bool {
45 self.credentials.remove(name).is_some()
46 }
47
48 pub fn list(&self) -> Vec<CredentialSummary> {
50 self.credentials.values().map(|c| CredentialSummary {
51 name: c.name.clone(),
52 auth_type: c.auth.method_name().to_string(),
53 created_at: c.created_at.to_rfc3339(),
54 tags: c.tags.clone(),
55 }).collect()
56 }
57
58 pub fn count(&self) -> usize {
60 self.credentials.len()
61 }
62
63 pub fn encrypt(&self, plaintext: &[u8]) -> ConnectResult<Vec<u8>> {
65 let key = self.encryption_key.as_ref()
66 .ok_or_else(|| ConnectError::EncryptionError("No encryption key set".into()))?;
67
68 let rng = SystemRandom::new();
69 let mut nonce_bytes = [0u8; 12];
70 rng.fill(&mut nonce_bytes)
71 .map_err(|_| ConnectError::EncryptionError("RNG failure".into()))?;
72
73 let nonce = aead::Nonce::assume_unique_for_key(nonce_bytes);
74 let mut in_out = plaintext.to_vec();
75
76 key.seal_in_place_append_tag(nonce, aead::Aad::empty(), &mut in_out)
77 .map_err(|_| ConnectError::EncryptionError("Encryption failed".into()))?;
78
79 let mut result = nonce_bytes.to_vec();
81 result.extend(in_out);
82 Ok(result)
83 }
84
85 pub fn decrypt(&self, ciphertext: &[u8]) -> ConnectResult<Vec<u8>> {
87 let key = self.encryption_key.as_ref()
88 .ok_or_else(|| ConnectError::EncryptionError("No encryption key set".into()))?;
89
90 if ciphertext.len() < 12 {
91 return Err(ConnectError::EncryptionError("Ciphertext too short".into()));
92 }
93
94 let (nonce_bytes, encrypted) = ciphertext.split_at(12);
95 let nonce = aead::Nonce::try_assume_unique_for_key(nonce_bytes)
96 .map_err(|_| ConnectError::EncryptionError("Invalid nonce".into()))?;
97
98 let mut in_out = encrypted.to_vec();
99 let plaintext = key.open_in_place(nonce, aead::Aad::empty(), &mut in_out)
100 .map_err(|_| ConnectError::EncryptionError("Decryption failed".into()))?;
101
102 Ok(plaintext.to_vec())
103 }
104}
105
106#[derive(Debug, Clone, serde::Serialize)]
108pub struct CredentialSummary {
109 pub name: String,
110 pub auth_type: String,
111 pub created_at: String,
112 pub tags: Vec<String>,
113}
114
115fn derive_key(passphrase: &str) -> ConnectResult<aead::LessSafeKey> {
117 use ring::pbkdf2;
118
119 let salt = b"agentic-connect-vault-v1"; let mut key_bytes = [0u8; 32];
121 pbkdf2::derive(
122 pbkdf2::PBKDF2_HMAC_SHA256,
123 std::num::NonZeroU32::new(100_000).unwrap(),
124 salt,
125 passphrase.as_bytes(),
126 &mut key_bytes,
127 );
128
129 let unbound = aead::UnboundKey::new(&aead::AES_256_GCM, &key_bytes)
130 .map_err(|_| ConnectError::EncryptionError("Key derivation failed".into()))?;
131
132 Ok(aead::LessSafeKey::new(unbound))
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::types::auth::AuthMethod;
139 use chrono::Utc;
140
141 #[test]
142 fn test_store_and_retrieve() {
143 let mut vault = CredentialVault::new();
144 let cred = StoredCredential {
145 id: uuid::Uuid::new_v4(),
146 name: "test-api".into(),
147 auth: AuthMethod::Bearer { token: "test-token-123".into() },
148 created_at: Utc::now(),
149 last_rotated: None,
150 tags: vec!["prod".into()],
151 };
152 vault.store(cred);
153 assert_eq!(vault.count(), 1);
154 let retrieved = vault.retrieve("test-api").unwrap();
155 assert_eq!(retrieved.name, "test-api");
156 }
157
158 #[test]
159 fn test_delete() {
160 let mut vault = CredentialVault::new();
161 let cred = StoredCredential {
162 id: uuid::Uuid::new_v4(), name: "temp".into(),
163 auth: AuthMethod::None, created_at: Utc::now(),
164 last_rotated: None, tags: vec![],
165 };
166 vault.store(cred);
167 assert!(vault.delete("temp"));
168 assert_eq!(vault.count(), 0);
169 }
170
171 #[test]
172 fn test_encrypt_decrypt_roundtrip() {
173 let vault = CredentialVault::with_encryption("my-secret-passphrase").unwrap();
174 let plaintext = b"sensitive-api-key-12345";
175 let ciphertext = vault.encrypt(plaintext).unwrap();
176 assert_ne!(&ciphertext, plaintext);
177 let decrypted = vault.decrypt(&ciphertext).unwrap();
178 assert_eq!(decrypted, plaintext);
179 }
180
181 #[test]
182 fn test_list_hides_secrets() {
183 let mut vault = CredentialVault::new();
184 vault.store(StoredCredential {
185 id: uuid::Uuid::new_v4(), name: "secret-key".into(),
186 auth: AuthMethod::Bearer { token: "SUPER_SECRET".into() },
187 created_at: Utc::now(), last_rotated: None, tags: vec![],
188 });
189 let list = vault.list();
190 assert_eq!(list.len(), 1);
191 assert_eq!(list[0].auth_type, "bearer");
192 let json = serde_json::to_string(&list[0]).unwrap();
194 assert!(!json.contains("SUPER_SECRET"));
195 }
196}