Skip to main content

bt_secure_storage/
secure_storage.rs

1use std::{collections::HashSet, error::Error, fs, path::PathBuf};
2use bt_file_cache::folder_manager::get_local_usr_data_path;
3use bt_logger::{get_error, get_fatal, log_error};
4use bt_string_utils::generate_url_safe_string;
5use keyring::Entry;
6
7// AES-GCM with 256-bit key
8use aes_gcm::{AeadCore, Aes256Gcm, Key, KeyInit, Nonce, aead::{Aead, OsRng}}; 
9use base91::{slice_decode, slice_encode};
10
11use pbkdf2::pbkdf2_hmac;
12//use base64::{engine::general_purpose, Engine};
13use whoami::username;
14
15use crate::hashing::get_hash_string_base64;
16
17/// Maximum size of a string for generating a service ID.
18const MAX_SIZE: usize = 256;
19
20const PATH_PREFIX: &str = "BT_PATH:";
21
22/// Represents a vault for storing secrets.
23pub struct SecretVault{
24    /// Prefix used to construct service IDs.
25    service_prefix: String,
26
27    /// User's username, used to gain access to the secret store.
28    user_name: String, 
29}
30
31impl SecretVault {
32    /// Generates a service ID by hashing the provided service name with the vault's prefix and salt.
33    /// 
34    /// # Returns
35    /// The hashed service ID as a string, truncated to `MAX_SIZE` characters.    
36    fn get_service_id(&self, service: &str) -> String{
37        let svr_name = format!("{}{}",self.service_prefix,service);
38        let svr = get_hash_string_base64(&svr_name);
39        svr.chars().take(MAX_SIZE).collect()
40    }
41
42    /// Creates a new `SecretVault` instance with the provided prefix and user's username.
43    ///
44    /// # Returns
45    /// The newly created `SecretVault` instance.    
46    pub fn new(srv_prefix: &str) -> Self{
47        let usr_name = username().chars().take(MAX_SIZE).collect(); 
48        Self { service_prefix: srv_prefix.to_owned(), user_name: usr_name }
49    }
50
51    /// Stores a secret with the provided service name and password.
52    ///
53    /// # Parameters
54    /// * `service`: The name of the service storing the secret.
55    /// * `secret`: The password to use for authentication.
56    ///
57    /// # Returns
58    /// A result indicating whether the operation was successful or not.    
59    pub fn store_secret(&self, service: &str, secret: &str) -> Result<(), Box<dyn Error>> {
60        let entry =  Entry::new(&self.get_service_id(service), &self.user_name)?; 
61        let set_result = entry.set_password(secret);
62        if set_result.is_ok(){
63            return Ok(())
64        }
65
66        #[cfg(target_os = "windows")]
67        {
68                use std::{fs, path::PathBuf};
69                use windows_dpapi::{encrypt_data, Scope};
70
71                let file_name = self.get_service_id(service);
72                let secrets_path = get_local_usr_data_path( Some(option_env!("CARGO_PKG_NAME").unwrap_or("bts_storage")),Some(".ces"),true)?;
73                // Encrypt for current user
74                let encrypted_secret =  encrypt_data(secret.as_bytes(), Scope::User)?;
75                // Store encrypted blob in a file
76                let mut file = PathBuf::from(secrets_path);
77                file.push(file_name);
78                fs::write(file.clone(), &encrypted_secret)?;
79                entry.set_password(&format!("{}{}",PATH_PREFIX, file.to_str().unwrap_or(".bt_stored_secret")))?;
80                return Ok(())
81        }
82
83        // Fallback for other OS
84        #[cfg(not(any(target_os = "windows")))]
85        {
86            return Err(get_error!("store_secret","Cannot save Secret. Error: {}",set_result.unwrap_err()).into())
87        }
88    }
89
90    /// Retrieves a secret by its name.
91    ///
92    /// # Parameters
93    /// * `service`: The name of the service storing the secret.
94    ///
95    /// # Returns
96    /// A result containing the retrieved password as a string, or an error if the operation failed.    
97    pub fn retrieve_secret(&self, service: &str) -> Result<String, Box<dyn Error>> {
98        let entry = Entry::new(&self.get_service_id(service), &self.user_name)?;
99        let secret = entry.get_password()?;
100        let result = secret.strip_prefix(PATH_PREFIX);
101        if result.is_none(){
102            return Ok(secret)
103        }
104
105
106        //read secret back
107        #[cfg(target_os = "windows")]
108        {   
109            use windows_dpapi::{Scope, decrypt_data};
110            let file = result.unwrap_or(&secret);
111            let path = PathBuf::from(file);     
112            //read secret back          
113            let encrypted_blob = fs::read(path)?;
114            let decrypt_secret = decrypt_data(&encrypted_blob, Scope::User)?;
115            return Ok(String::from_utf8(decrypt_secret)?)
116        }
117
118        Ok(secret)
119    }
120
121    /// Stores an attribute set with the provided service name and attributes.
122    ///
123    /// # Parameters
124    /// * `service`: The name of the service storing the attribute set.
125    /// * `attributes`: A set of strings representing the attribute values.
126    ///
127    /// # Returns
128    /// A result indicating whether the operation was successful or not.    
129    pub fn store_attribute_set(&self, service: &str, attributes: &HashSet<String>) -> Result<(), Box<dyn Error>> {
130        let map = serde_json::to_string(attributes)?;
131        self.store_secret(service, &map)
132    }
133
134    /// Retrieves an attribute set by its name.
135    ///
136    /// # Parameters
137    /// * `service`: The name of the service storing the attribute set.
138    ///
139    /// # Returns
140    /// A result containing the retrieved attribute set as a string, or an error if the operation failed.    
141    pub fn retrieve_attribute_set(&self,service: &str) -> Result<HashSet<String>, Box<dyn Error>>{
142        let map =self.retrieve_secret(service)?;
143        let set_from_values: HashSet<String> = serde_json::from_str(&map)?;
144        Ok(set_from_values)
145    }
146
147
148    /// Deletes a secret by its name.
149    ///
150    /// # Parameters
151    /// * `service`: The name of the service storing the secret.
152    ///
153    /// # Returns
154    /// A result indicating whether the operation was successful or not.    
155    pub fn delete_secret(&self, service: &str) -> Result<(), Box<dyn Error>> {
156        let entry = Entry::new(&self.get_service_id(service), &self.user_name)?;
157        let secret = entry.get_password()?;
158        let result = secret.strip_prefix(PATH_PREFIX);            
159        let _r = entry.delete_credential()?;
160        if result.is_none(){
161            return Ok(())
162        }
163
164        let _r = fs::remove_file(result.unwrap_or(&secret))?;
165        Ok(())
166    }
167}
168
169/// Number of iterations for password-based key derivation.
170const ITERATIONS: u32 = 100_000;
171
172/// Length of the secret key in bytes.
173const KEY_LEN: usize = 32;
174
175/// Represents a cipher for encrypting and decrypting secrets.
176#[derive(Debug)]
177pub struct SecretCipher{
178    salt: String,
179}
180
181impl SecretCipher {
182    /// Creates a new `SecretCipher` instance with a randomly generated salt.
183    ///
184    /// # Returns
185    /// The newly created `SecretCipher` instance.    
186    pub fn new() -> Self{
187        const SECRET_CIPHER_SERVICE_PREFIX: &str = "secret_cipher_service";
188        const SALT: &str = "SALT";
189        let sv = SecretVault::new(SECRET_CIPHER_SERVICE_PREFIX);
190        let s = sv.retrieve_secret(SALT);
191
192        let cipher_salt = if s.is_ok() { 
193                                        s.unwrap() 
194                                    } else {
195                                        let new_salt = generate_url_safe_string(12);
196                                        sv.store_secret(SALT, &new_salt).expect(&get_fatal!("new","Cannot securely save encryption data!"));
197                                        new_salt
198                                    };
199        SecretCipher{ salt:cipher_salt }
200    }
201
202    /// Derives a key from the provided password using password-based key derivation.
203    ///
204    /// # Parameters
205    /// * `password`: The password to use for key derivation.
206    ///
207    /// # Returns
208    /// The derived key as a byte array.    
209    fn derive_key(&self, password: &str) -> [u8; KEY_LEN] {
210        let mut key = [0u8; KEY_LEN];
211        pbkdf2_hmac::<sha2::Sha256>(password.as_bytes(), self.salt.as_bytes(), ITERATIONS, &mut key);
212        key
213    }
214
215    /// Encrypts the provided secret using the derived key and random nonce.
216    ///
217    /// # Parameters
218    /// * `secret`: The secret to encrypt.
219    /// * `key_str`: The password used for encryption.
220    ///
221    /// # Returns
222    /// A tuple containing the encrypted ciphertext as a base91-encoded string, and the random nonce as a base64-encoded string.    
223    pub fn encrypt_secret(&self, secret: &str, key_str: &str) -> Result<(String, String),Box< dyn Error>> {
224        // Hash the key string to get a 32-byte key
225        let key_hash: &[u8; 32] = &self.derive_key(key_str);
226        let key: &Key<Aes256Gcm>  = key_hash.into(); 
227        let cipher = Aes256Gcm::new(key);
228
229        // Generate a random 12-byte nonce
230        //let nonce_bytes: [u8; 12] =  rand::rng().random();
231        let nonce =  Aes256Gcm::generate_nonce(&mut OsRng); //Nonce::from_slice(&nonce_bytes);
232
233        // Encrypt the secret
234        let ciphertext = match cipher.encrypt(&nonce, secret.as_bytes()){
235            Ok(c) => c,
236            Err(e) => return Err(get_error!("encrypt_secret","Unable to encrypt secret. Error: {}",e).into()),
237        };
238
239        // Return base64-encoded ciphertext and nonce
240        //Ok( ( general_purpose::URL_SAFE_NO_PAD.encode(&ciphertext),  general_purpose::URL_SAFE_NO_PAD.encode(&nonce) ) ) 
241        // Return base91-encoded ciphertext and nonce
242        Ok( ( String::from_utf8(slice_encode(&ciphertext))?,  String::from_utf8(slice_encode(&nonce))? ) ) 
243    }
244
245    /// Decrypts the provided ciphertext using the derived key and random nonce.
246    ///
247    /// # Parameters
248    /// * `ciphertext_b64`: The base91-encoded ciphertext to decrypt.
249    /// * `nonce_b91`: The base91-encoded nonce used for decryption.
250    /// * `key_str`: The password used for decryption.
251    ///
252    /// # Returns
253    /// A result containing the decrypted plaintext as a string, or an error if the operation failed.    
254    pub fn decrypt_secret(&self, ciphertext_b91: &str, nonce_b91: &str, key_str: &str) -> Result<String, Box< dyn Error>>{
255        let key_hash: &[u8; 32] = &self.derive_key(key_str); 
256        let key: &Key<Aes256Gcm> = key_hash.into(); 
257        let cipher = Aes256Gcm::new(key);
258
259        let nonce_bytes = slice_decode(nonce_b91.as_bytes()); //general_purpose::URL_SAFE_NO_PAD.decode(nonce_b64)?;
260        let nonce = Nonce::from_slice(&nonce_bytes);
261
262        let ciphertext = slice_decode(ciphertext_b91.as_bytes()); //general_purpose::URL_SAFE_NO_PAD.decode(ciphertext_b64)?;
263        match cipher.decrypt(nonce, ciphertext.as_ref()){
264            Ok(plaintext) => Ok(String::from_utf8(plaintext)?),
265            Err(e) => {
266                log_error!("decrypt_secret","");
267                Err(get_error!("decrypt_secret","Decryption failed. Error: {}", e).into())
268            },
269        }
270    }
271}
272
273
274#[cfg(test)]
275mod enc_dec_tests {
276    use bt_logger::{build_logger, log_verbose};
277
278    use super::*;
279
280    #[test]
281    fn test_encrypt_decrypt_roundtrip() {
282        build_logger("BACHUETECH", "enc_dec_tests", bt_logger::LogLevel::VERBOSE, bt_logger::LogTarget::STD_ERROR, None);
283
284        let cipher = SecretCipher::new();
285        let key = "my-auth0-client-secret";
286        let secret = "super-sensitive-token";
287
288        let (encrypted, nonce) = cipher.encrypt_secret(secret, key).unwrap();
289        let decrypted = cipher.decrypt_secret(&encrypted, &nonce, key);
290
291        assert_eq!(decrypted.unwrap(), secret);
292    }
293
294    #[test]
295    fn test_decrypt_with_wrong_key_fails() {
296        build_logger("BACHUETECH", "enc_dec_tests", bt_logger::LogLevel::VERBOSE, bt_logger::LogTarget::STD_OUT, None);
297        let cipher = SecretCipher::new();        
298        let key = "correct-key";
299        let wrong_key = "wrong-key";
300        let secret = "top-secret";
301
302        let (encrypted, nonce) = cipher.encrypt_secret(secret, key).unwrap();
303        let result = cipher.decrypt_secret(&encrypted, &nonce, wrong_key);
304        log_verbose!("test_decrypt_with_wrong_key_fails","DEC: {:?}",result);
305        assert!(result.is_err(), "Decryption should fail with wrong key");
306    }
307
308    #[test]
309    fn test_decrypt_outputs_different_times_success() {
310        build_logger("BACHUETECH", "enc_dec_tests", bt_logger::LogLevel::VERBOSE, bt_logger::LogTarget::STD_ERROR, None);
311        let cipher = SecretCipher::new();        
312        let key = "same-key";
313        let secret = "same-secret";
314
315        let (enc1, _) = cipher.encrypt_secret(secret, key).unwrap();        
316        let (enc2, _) = cipher.encrypt_secret(secret, key).unwrap();
317
318        assert_ne!(enc1, enc2, "Ciphertext should not differ due to dec times");
319    }
320
321
322    #[test]
323    fn test_decrypt_outputs_different_ciphertext_each_time() {
324        build_logger("BACHUETECH", "enc_dec_tests", bt_logger::LogLevel::VERBOSE, bt_logger::LogTarget::STD_ERROR, None);
325        let cipher = SecretCipher::new();        
326        let key = "same-key";
327        let secret = "same-secret";
328
329        let (enc1, nonce) = cipher.encrypt_secret(secret, key).unwrap();
330        let cipher = SecretCipher::new();         
331        let dec1 = cipher.decrypt_secret(&enc1, &nonce, key);
332        let cipher = SecretCipher::new();
333        let dec2 = cipher.decrypt_secret(&enc1, &nonce, key);  
334
335        log_verbose!("test_decrypt_outputs_different_ciphertext_each_time","dec1: {:?} dec2: {:?}",dec1,dec2);
336        let d1 = dec1.unwrap();
337        assert_eq!(d1, secret);
338        let d2 = dec2.unwrap();
339        log_verbose!("test_decrypt_outputs_different_ciphertext_each_time","d1: '{}'. d2: '{}'",d1,d2);
340        assert_eq!(d1, d2, "Ciphertext should not differ secret");
341    }    
342}
343
344
345#[cfg(test)]
346mod secret_vault_tests {
347    use super::*;
348    use std::{collections::HashSet, sync::Once};
349
350    use bt_logger::{build_logger, log_verbose, LogLevel, LogTarget};
351    use ctor::dtor;
352
353   
354    static INIT: Once = Once::new();
355    fn ini_log() {
356        INIT.call_once(|| {
357            build_logger("BACHUETECH", "UNIT TEST RUST JOB_SEEK_TRK_DX", LogLevel::VERBOSE, LogTarget::STD_ERROR, None );     
358        });
359    }
360
361    #[test]
362    fn test_new_secret_vault() {
363        let vault = SecretVault::new("test_prefix");
364        assert_eq!(vault.service_prefix, "test_prefix");
365        // Note: user_name is tested via whoami::username which is hard to mock
366        // so we just check it's not empty
367        assert!(!vault.user_name.is_empty());
368    }
369
370    #[test]
371    fn test_get_service_id() {
372        let vault = SecretVault::new("test_prefix");
373        let service_name = "test_service";
374        let service_id = vault.get_service_id(service_name);
375        
376        // Check that the result is a valid string and not empty
377        assert!(!service_id.is_empty());
378        
379        // Check that it's truncated to MAX_SIZE
380        assert!(service_id.len() <= MAX_SIZE);
381    }
382
383    #[test]
384    fn test_get_service_id_with_long_service_name() {
385        ini_log();
386        let vault = SecretVault::new("test_prefix");
387        let long_service_name = "a".repeat(MAX_SIZE + 300); // Long service name
388        let service_id = vault.get_service_id(&long_service_name);
389        // Should still be truncated to <= MAX_SIZE
390        assert!(service_id.len() <= MAX_SIZE);
391    }
392
393    #[test]
394    fn test_store_secret() {
395        // Note: This test requires a real keyring backend to be available
396        let vault = SecretVault::new("test_prefix");
397        let service_name = "test_service";
398        let secret = "test_secret";
399        
400        let result = vault.store_secret(service_name, secret);
401        assert!(result.is_ok()); // or assert!(result.is_err()) depending on environment
402    }
403
404
405    #[test]
406    fn test_store_long_secret() {
407        ini_log();
408        // Note: This test requires a real keyring backend to be available
409        let vault = SecretVault::new("test_prefix");
410        let service_name = format!("{}{}","bt_secure_storage_token","test_service_long");
411        let secret = "easyasdfAiOiJKV1QiLCJub25jZSI6I1234GN5NUlYVmI3eG9hMEZJOUdjMnVJVnIzRjZ5anhhTlRQd3dhQTIwYkkiLCJhbGciOiJSUzI1NiIsIng1dCI6InJ0c0ZULWItN0x1WTdEVlllU05LY0lKN1ZuYyIsImtpZCI6InJ0c0ZULWItN0x1WTdEVlllU05LY0lKN1ZuYyJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9hMDA1ZWExNi1lZmZkLTRiMWMtYWRjMi00Njc2NDE4NjEwYzIvIiwiaWF0IjoxNzYzOTkxMDI4LCJuYmYiOjE3NjM5OTEwMjgsImV4cCI6MTc2Mzk5NjEwOSwiYWNjdCI6MCwiYWNyIjoiMSIsImFpbyI6IkFZUUFlLzhhQUFBQTZ2a3ZPZFJrSXNteUtIdEcvakYxd2dTRGpZeFl6c3hGNGl3WDN2QndrU1QrT0NRMEhJckdMR3Y2R0RzRU9jRm5DTkdBS1pOOStwRzNJd1RLUXlEdEdqaDExUWVwSmI3cDhzQWRFenVIRHVlbHFaUFpwaUh1RTBlY09SdTBheW9DUDVDMjRnNFVDWkMrSSs2Sy9tbDZxQW04aHE2Z3Rma0U1dk92R3N6cCtjaz0iLCJhbXIiOlsib3RwIl0sImFwcF9kaXNwbGF5bmFtZSI6IkpvYiBUYWNrbGUiLCJhcHBpZCI6ImE5NmJmNzFjLTNhYWEtNDc4NS05YjUxLWQ3OThmYWM1MWViYiIsImFwcGlkYWNyIjoiMCIsImVtYWlsIjoiY2VhbHZhcmV6cm9AaG90bWFpbC5jb20iLCJpZHAiOiJtYWlsIiwiaWR0eXAiOiJ1c2VyIiwiaXBhZGRyIjoiMTczLjIzMy4yLjI3IiwibmFtZSI6IkNlc2FyIiwib2lkIjoiMjY2NjUzMGUtOTJmOC00ZTQzLTk2OTQtOGVhM2Y1OGZlYzc0IiwicGxhdGYiOiIzIiwicHVpZCI6IjEwMDMyMDA1NThFM0Y4NTUiLCJyaCI6IjEuQWJnQUZ1b0ZvUDN2SEV1dHdrWjJRWVlRd2dNQUFBQUFBQUFBd0FBQUFBQUFBQURWQWNPNEFBLiIsInNjcCI6ImVtYWlsIG9wZW5pZCBwcm9maWxlIiwic2lkIjoiMDBhYjhhMjktNWQwOC01MTYxLTdmNzEtNWI5NTQ5MGY1NmU1Iiwic3ViIjoiWFFlWTFpV0NWRTJRa21rSndycllTVElVUGhQNXY0eGM3anphcEQwR1M3MCIsInRlbmFudF9yZWdpb25fc2NvcGUiOiJOQSIsInRpZCI6ImEwMDVlYTE2LWVmZmQtNGIxYy1hZGMyLTQ2NzY0MTg2MTBjMiIsInVuaXF1ZV9uYW1lIjoibWFpbCNjZWFsdmFyZXpyb0Bob3RtYWlsLmNvbSIsInV0aSI6Ik1tQnZPR0g4aDBlRGpGb2tiQzREQUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbImI3OWZiZjRkLTNlZjktNDY4OS04MTQzLTc2YjE5NGU4NTUwOSJdLCJ4bXNfYWNkIjoxNzYzNzM2MjgwLCJ4bXNfYWN0X2ZjdCI6IjMgOSIsInhtc19mdGQiOiJUNUxkQnppS0VSU1pibWZLMTFhcWEycDltWmFIXzRrUmJlX0Z5bjZnbGpzQmRYTmxZWE4wTFdSemJYTSIsInhtc19pZHJlbCI6IjE0IDEiLCJ4bXNfc3QiOnsic3ViIjoiWjA4ZW5uM1c5VW5ac3dvR1R0NERWTDB5U2Y1Q2dfLXhIbGNDRTlNdGZhWSJ9LCJ4bXNfc3ViX2ZjdCI6IjMgMTYiLCJ4bXNfdGNkdCI6MTc2MzY4MjQ3MSwieG1zX3RudF9mY3QiOiIzIDgifQ.NrvFEXwMEpublzKJToNkbj6x8QXzOmOHKfzIxLEkq4b_cjLEPx4Y5qr32AmIQ3n-xysWO93sZDO7tBFeAMi90hso9LkGJ321O7WsjGFaQ4fBog_UsX-f2NII4TyS1soEshlL1odQM-2WItPV-EyF9iuiJqhdsd-6dC38_0V1G4EZz0IrmlIfYlqRBHfEtf7Mam9KHgZ5Q7fUc2bJA1sfl61SzADaAsa51vo1b8v87Db86NqfdedviEI3ktjrZrJ8eMX0Ekx8sNM9o2jfUkNMb445kd4J4VUiCBrgVOB5loMD6Zrc9DtmjJjKeHSjXbLp-r1ECDnJm0EvLzigj555555";
412        
413        let result = vault.store_secret(&service_name, secret);
414        assert!(result.is_ok()); // or assert!(result.is_err()) depending on environment
415    }
416
417    #[test]
418    fn test_retrieve_unknown_secret() {
419        // Note: This test requires a real keyring backend to be available
420        let vault = SecretVault::new("test_prefix");
421        let service_name = "invalid_test_service_unknown";
422        
423        // This will fail if no keyring backend is available or if no secret exists
424        let result = vault.retrieve_secret(service_name);
425        assert!(result.is_err()); // Should fail because no secret was stored
426    }
427
428    #[test]
429    fn test_store_attribute_set() {
430        // Note: This test requires a real keyring backend to be available
431        let vault = SecretVault::new("test_prefix");
432        let mut attributes = HashSet::new();
433        attributes.insert("attr1".to_string());
434        attributes.insert("attr2".to_string());
435        let service_name = "test_service";
436        
437        let result = vault.store_attribute_set(service_name, &attributes);
438        assert!(result.is_ok());
439    }
440
441    #[test]
442    fn test_delete_secret() {
443        ini_log();
444        // Note: This test requires a real keyring backend to be available
445        let vault = SecretVault::new("test_prefix");
446        let service_name = "test_service_delete";
447        let _ = vault.store_secret(service_name, "delete");
448        
449        let result = vault.delete_secret(service_name);
450        log_verbose!("test_delete_secret","delete rest: {:?}",result);
451        assert!(result.is_ok());     
452    }
453
454
455    #[test]
456    fn test_store_and_retrieve_secret() {
457        ini_log();
458        // Note: This test requires a real keyring backend to be available
459        let vault = SecretVault::new("test_prefix");
460        let service_name = "test_service_str";
461        let secret = "test_secret";
462        
463        // Store a secret
464        let store_result = vault.store_secret(service_name, secret);
465        assert!(store_result.is_ok());
466        
467        // Retrieve the secret
468        let retrieve_result = vault.retrieve_secret(service_name);
469log_verbose!("test_store_and_retrieve_secret","Retrieved Secret: {:?}",retrieve_result);
470        assert!(retrieve_result.is_ok());
471        assert_eq!(retrieve_result.unwrap(), secret);
472    }
473
474    #[test]
475    fn test_store_and_retrieve_attribute_set() {
476        ini_log();
477        // Note: This test requires a real keyring backend to be available
478        let vault = SecretVault::new("test_prefix");
479        let mut attributes = HashSet::new();
480        attributes.insert("attr1".to_string());
481        attributes.insert("attr2".to_string());
482        let service_name = "test_service_set";
483        
484        // Store an attribute set
485        let store_result = vault.store_attribute_set(service_name, &attributes);
486        assert!(store_result.is_ok());
487        
488        // Retrieve the attribute set
489        let retrieve_result = vault.retrieve_attribute_set(service_name);
490        log_verbose!("test_store_and_retrieve_attribute_set","Retrieve Result: {:?}",retrieve_result);
491        assert!(retrieve_result.is_ok());
492        assert_eq!(retrieve_result.unwrap(), attributes);
493    }
494
495    #[dtor]
496    fn after_all() {
497        ini_log();
498        let vault = SecretVault::new("test_prefix");
499        vault.delete_secret("test_service_set");
500        vault.delete_secret("test_service_str");
501        vault.delete_secret("test_service");
502        vault.delete_secret("invalid_test_service_unknown");
503        log_verbose!("clean_storage","Done Cleaning");
504    }
505}