Skip to main content

hana_vault/
lib.rs

1//! # Hana Vault - Encrypted SQLite Credential Manager
2//!
3//! A secure, encrypted vault for storing SSH credentials, hosts, and sensitive data using SQLite with AES-256-GCM encryption.
4//!
5//! ## Features
6//!
7//! - **Encrypted SQLite Storage**: All data is stored in an encrypted in-memory SQLite database
8//! - **Binary Encryption**: Vaults can be exported as encrypted bytes
9//! - **Autonomous Migrations**: Schema versioning with automatic migration system
10//! - **Multiple Credential Types**: Support for username/password, RSA, OpenSSH, Ed25519, ECDSA, and certificate-based auth
11//! - **Host Management**: Store hosts with startup commands, environment variables, and custom encodings
12//! - **Industry-Standard Crypto**: AES-256-GCM symmetric encryption
13//! - **Key-Based API**: Supply your own encryption key (derive from password using Argon2id in your application)
14//! - **Zero Knowledge**: Raw SQLite database is never exposed, only encrypted formats
15//!
16//! ## Usage
17//!
18//! ```rust
19//! use hana_vault::{Vault, Host, Credential, SecretKey};
20//! # use hana_vault::error::Result;
21//!
22//! # fn main() -> Result<()> {
23//! // Generate or derive a 256-bit key (in production, derive from password using Argon2id)
24//! let key = SecretKey::random();
25//!
26//! // Create a new vault with the key
27//! let vault = Vault::new(key.clone())?;
28//!
29//! // Add a host
30//! let mut host = Host::new("Production Server".to_string(), "prod.example.com".to_string(), 22);
31//! host.add_env_var("PATH".to_string(), "/usr/local/bin:/usr/bin".to_string());
32//! host = host.with_startup_command("source ~/.profile".to_string());
33//! vault.add_host(&host)?;
34//!
35//! // Add credentials
36//! let cred = Credential::new_username_password(
37//!     "Admin Credentials".to_string(),
38//!     "admin".to_string(),
39//!     "super_secret".to_string(),
40//! );
41//! vault.add_credential(&cred)?;
42//!
43//! // Link credential to host
44//! vault.link_credential_to_host(host.id, cred.id, true)?;
45//!
46//! // Save to encrypted file (manual implementation)
47//! # use std::path::Path;
48//! # use std::fs::File;
49//! # use std::io::Write;
50//! # let temp_dir = std::env::temp_dir();
51//! # let path = temp_dir.join("vault.hev");
52//! let encrypted_bytes = vault.export_to_bytes()?;
53//! let mut file = File::create(&path)?;
54//! file.write_all(&encrypted_bytes)?;
55//!
56//! // Load from file (manual implementation)
57//! # use std::io::Read;
58//! let mut file = File::open(&path)?;
59//! let mut encrypted_data = Vec::new();
60//! file.read_to_end(&mut encrypted_data)?;
61//! let loaded = Vault::load_from_bytes(&encrypted_data, key)?;
62//! # Ok(())
63//! # }
64//! ```
65//!
66//! ## Security Features
67//!
68//! - **Encrypted Storage**: SQLite database encrypted at rest with AES-256-GCM
69//! - **Key-Based API**: You control key derivation (recommended: Argon2id with 600,000+ iterations)
70//! - **Memory-Only SQLite**: Database only exists in memory, never on disk
71//! - **Binary Encryption**: All exports use authenticated encryption (AES-256-GCM)
72//! - **Memory Safety**: Sensitive data automatically zeroized on drop
73//! - **Checksum Verification**: SHA-256 checksums prevent data corruption
74//! - **No Plaintext Storage**: Raw SQLite database never written to disk
75//!
76
77pub mod binary;
78pub mod crypto;
79pub mod error;
80pub mod models;
81pub mod schema;
82pub mod vault;
83
84// Re-export main types for convenience
85pub use crypto::SecretKey;
86pub use error::{Result, VaultError};
87pub use models::{Credential, CredentialType, Host, SecureString};
88pub use vault::Vault;
89
90// Re-export crypto constants for advanced users
91pub use crypto::{
92    NONCE_SIZE, TAG_SIZE, KEY_SIZE,
93    VAULT_FORMAT_VERSION, VAULT_MAGIC,
94};
95
96// Re-export schema version
97pub use schema::CURRENT_VERSION as SCHEMA_VERSION;
98
99/// Library version
100pub const VERSION: &str = env!("CARGO_PKG_VERSION");
101
102#[cfg(test)]
103mod integration_tests {
104    use super::*;
105    use tempfile::NamedTempFile;
106
107    #[test]
108    fn test_full_workflow() -> Result<()> {
109        // Create vault with a random key
110        let key = SecretKey::random();
111        let vault = Vault::new(key.clone())?;
112
113        // Add hosts
114        let mut web_server = Host::new(
115            "Web Server".to_string(),
116            "web.example.com".to_string(),
117            22,
118        );
119        web_server = web_server.with_username("deploy".to_string());
120        web_server = web_server.with_startup_command("cd /var/www && source env.sh".to_string());
121        web_server.add_env_var("NODE_ENV".to_string(), "production".to_string());
122        web_server.add_env_var("PORT".to_string(), "3000".to_string());
123
124        let db_server = Host::new(
125            "Database Server".to_string(),
126            "db.example.com".to_string(),
127            22,
128        );
129
130        vault.add_host(&web_server)?;
131        vault.add_host(&db_server)?;
132
133        // Add credentials
134        let ssh_key_cred = Credential::new_ssh_key(
135            "Deploy Key".to_string(),
136            CredentialType::OpenSsh,
137            "-----BEGIN OPENSSH PRIVATE KEY-----\ntest_key_data\n-----END OPENSSH PRIVATE KEY-----".to_string(),
138            Some("ssh-ed25519 AAAAC3... user@example.com".to_string()),
139            None,
140        );
141
142        let password_cred = Credential::new_username_password(
143            "Admin Password".to_string(),
144            "admin".to_string(),
145            "super_secure_password_123!".to_string(),
146        );
147
148        vault.add_credential(&ssh_key_cred)?;
149        vault.add_credential(&password_cred)?;
150
151        // Link credentials to hosts
152        vault.link_credential_to_host(web_server.id, ssh_key_cred.id, true)?;
153        vault.link_credential_to_host(db_server.id, password_cred.id, true)?;
154
155        // Test retrieval
156        let hosts = vault.list_hosts()?;
157        assert_eq!(hosts.len(), 2);
158
159        let credentials = vault.list_credentials()?;
160        assert_eq!(credentials.len(), 2);
161
162        let web_server_creds = vault.get_host_credentials(web_server.id)?;
163        assert_eq!(web_server_creds.len(), 1);
164        assert_eq!(web_server_creds[0].0.name, "Deploy Key");
165        assert!(web_server_creds[0].1); // is_default
166
167        // Test export to bytes
168        let encrypted_bytes = vault.export_to_bytes()?;
169        assert!(!encrypted_bytes.is_empty());
170
171        // Test load from bytes
172        let loaded_vault = Vault::load_from_bytes(&encrypted_bytes, key)?;
173
174        let loaded_hosts = loaded_vault.list_hosts()?;
175        assert_eq!(loaded_hosts.len(), 2);
176
177        // Verify environment variables were preserved
178        let loaded_web = loaded_hosts.iter().find(|h| h.name == "Web Server").unwrap();
179        assert_eq!(loaded_web.environment_variables.get("NODE_ENV"), Some(&"production".to_string()));
180        assert_eq!(loaded_web.environment_variables.get("PORT"), Some(&"3000".to_string()));
181        assert_eq!(loaded_web.startup_command.as_deref(), Some("cd /var/www && source env.sh"));
182
183        Ok(())
184    }
185
186    #[test]
187    fn test_file_roundtrip() -> Result<()> {
188        let temp_file = NamedTempFile::new().unwrap();
189        let path = temp_file.path();
190
191        // Create and save vault
192        let key = SecretKey::random();
193        let vault = Vault::new(key.clone())?;
194
195        let host = Host::new("Test Server".to_string(), "test.example.com".to_string(), 2222);
196        vault.add_host(&host)?;
197
198        let encrypted_data = vault.export_to_bytes()?;
199        std::fs::write(path, &encrypted_data)?;
200
201        // Load vault
202        let file_data = std::fs::read(path)?;
203        let loaded = Vault::load_from_bytes(&file_data, key)?;
204
205        let hosts = loaded.list_hosts()?;
206        assert_eq!(hosts.len(), 1);
207        assert_eq!(hosts[0].name, "Test Server");
208        assert_eq!(hosts[0].port, 2222);
209
210        Ok(())
211    }
212
213    #[test]
214    fn test_wrong_key() {
215        let correct_key = SecretKey::random();
216        let wrong_key = SecretKey::random();
217        
218        let vault = Vault::new(correct_key).unwrap();
219        let bytes = vault.export_to_bytes().unwrap();
220
221        let result = Vault::load_from_bytes(&bytes, wrong_key);
222        assert!(matches!(result, Err(VaultError::InvalidKey)));
223    }
224
225    #[test]
226    fn test_update_operations() -> Result<()> {
227        let key = SecretKey::random();
228        let vault = Vault::new(key)?;
229
230        // Add and update host
231        let mut host = Host::new("Server".to_string(), "example.com".to_string(), 22);
232        vault.add_host(&host)?;
233
234        host.name = "Updated Server".to_string();
235        host.port = 2222;
236        host.add_env_var("NEW_VAR".to_string(), "value".to_string());
237        vault.update_host(&host)?;
238
239        let updated = vault.get_host(host.id)?.unwrap();
240        assert_eq!(updated.name, "Updated Server");
241        assert_eq!(updated.port, 2222);
242        assert_eq!(updated.environment_variables.get("NEW_VAR"), Some(&"value".to_string()));
243
244        // Add and update credential
245        let mut cred = Credential::new_username_password(
246            "Cred".to_string(),
247            "user".to_string(),
248            "pass".to_string(),
249        );
250        vault.add_credential(&cred)?;
251
252        cred.name = "Updated Cred".to_string();
253        vault.update_credential(&cred)?;
254
255        let updated_cred = vault.get_credential(cred.id)?.unwrap();
256        assert_eq!(updated_cred.name, "Updated Cred");
257
258        Ok(())
259    }
260
261    #[test]
262    fn test_delete_operations() -> Result<()> {
263        let key = SecretKey::random();
264        let vault = Vault::new(key)?;
265
266        let host = Host::new("Server".to_string(), "example.com".to_string(), 22);
267        vault.add_host(&host)?;
268
269        let cred = Credential::new_username_password(
270            "Cred".to_string(),
271            "user".to_string(),
272            "pass".to_string(),
273        );
274        vault.add_credential(&cred)?;
275
276        // Delete host
277        vault.delete_host(host.id)?;
278        assert!(vault.get_host(host.id)?.is_none());
279
280        // Delete credential
281        vault.delete_credential(cred.id)?;
282        assert!(vault.get_credential(cred.id)?.is_none());
283
284        Ok(())
285    }
286
287    #[test]
288    fn test_multiple_credentials_per_host() -> Result<()> {
289        let key = SecretKey::random();
290        let vault = Vault::new(key)?;
291
292        let host = Host::new("Server".to_string(), "example.com".to_string(), 22);
293        vault.add_host(&host)?;
294
295        let cred1 = Credential::new_username_password(
296            "Cred 1".to_string(),
297            "user1".to_string(),
298            "pass1".to_string(),
299        );
300        let cred2 = Credential::new_username_password(
301            "Cred 2".to_string(),
302            "user2".to_string(),
303            "pass2".to_string(),
304        );
305
306        vault.add_credential(&cred1)?;
307        vault.add_credential(&cred2)?;
308
309        vault.link_credential_to_host(host.id, cred1.id, true)?;
310        vault.link_credential_to_host(host.id, cred2.id, false)?;
311
312        let host_creds = vault.get_host_credentials(host.id)?;
313        assert_eq!(host_creds.len(), 2);
314
315        // First should be default (sorted by is_default DESC)
316        assert_eq!(host_creds[0].0.id, cred1.id);
317        assert!(host_creds[0].1);
318
319        Ok(())
320    }
321
322
323    #[test]
324    fn test_all_credential_types() -> Result<()> {
325        let key = SecretKey::random();
326        let vault = Vault::new(key)?;
327
328        let types = vec![
329            CredentialType::UsernamePassword,
330            CredentialType::Rsa,
331            CredentialType::OpenSsh,
332            CredentialType::Ed25519,
333            CredentialType::Ecdsa,
334            CredentialType::Certificate,
335            CredentialType::Custom,
336        ];
337
338        for cred_type in types {
339            let cred = Credential::new_ssh_key(
340                format!("{:?} Key", cred_type),
341                cred_type,
342                "test_private_key".to_string(),
343                Some("test_public_key".to_string()),
344                None,
345            );
346            vault.add_credential(&cred)?;
347
348            let retrieved = vault.get_credential(cred.id)?.unwrap();
349            assert_eq!(retrieved.credential_type, cred_type);
350        }
351
352        let all_creds = vault.list_credentials()?;
353        assert_eq!(all_creds.len(), 7);
354
355        Ok(())
356    }
357}