portkey/
vault.rs

1use anyhow::{Context, Result};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use sodiumoxide::crypto::secretbox;
5use sodiumoxide::crypto::pwhash::argon2id13;
6use std::fs;
7use std::io::Write;
8use std::os::unix::fs::PermissionsExt;
9use std::path::PathBuf;
10
11use crate::crypto::{generate_salt, MasterKey};
12use crate::models::{Server, VaultData};
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct VaultFile {
16    pub salt: argon2id13::Salt,
17    pub nonce: secretbox::Nonce,
18    pub ciphertext: Vec<u8>,
19    pub created_at: DateTime<Utc>,
20    pub updated_at: DateTime<Utc>,
21}
22
23pub struct Vault {
24    data_path: PathBuf,
25    master_key: Option<MasterKey>,
26    data: Option<VaultData>,
27}
28
29impl Vault {
30    pub fn new() -> Result<Self> {
31        let data_dir = dirs::data_dir()
32            .context("Failed to find data directory")?
33            .join("portkey");
34        
35        if !data_dir.exists() {
36            fs::create_dir_all(&data_dir)?;
37        }
38
39        let data_path = data_dir.join("vault.dat");
40
41        Ok(Self {
42            data_path,
43            master_key: None,
44            data: None,
45        })
46    }
47
48    pub fn exists(&self) -> bool {
49        self.data_path.exists()
50    }
51
52    pub fn unlock(&mut self, password: Option<&str>) -> Result<()> {
53        if !self.exists() {
54            return Err(anyhow::anyhow!("Vault does not exist"));
55        }
56
57        let vault_file = self.load_vault_file()?;
58        
59        // Try to decrypt with password if provided
60        if let Some(password) = password {
61            let master_key = MasterKey::from_password(password, &vault_file.salt)?;
62            
63            // Check if this looks like encrypted data by attempting decryption
64            let decrypted_data = master_key.decrypt(&vault_file.ciphertext, &vault_file.nonce)?;
65            let vault_data: VaultData = serde_json::from_slice(&decrypted_data)
66                .context("Failed to deserialize vault data")?;
67
68            self.master_key = Some(master_key);
69            self.data = Some(vault_data);
70        } else {
71            // No password provided, assume unencrypted vault
72            let vault_data: VaultData = serde_json::from_slice(&vault_file.ciphertext)
73                .context("Failed to deserialize vault data - try providing a password")?;
74            
75            self.master_key = None;
76            self.data = Some(vault_data);
77        }
78
79        Ok(())
80    }
81
82    pub fn create(&mut self, password: Option<&str>) -> Result<()> {
83        if self.exists() {
84            return Err(anyhow::anyhow!("Vault already exists"));
85        }
86
87        let vault_data = VaultData::new();
88        let serialized = serde_json::to_vec(&vault_data)?;
89
90        let vault_file = if let Some(password) = password {
91            // Password-protected vault
92            let salt = generate_salt();
93            let master_key = MasterKey::from_password(password, &salt)?;
94            let (nonce, ciphertext) = master_key.encrypt(&serialized);
95            
96            VaultFile {
97                salt,
98                nonce,
99                ciphertext,
100                created_at: Utc::now(),
101                updated_at: Utc::now(),
102            }
103        } else {
104            // Unencrypted vault (no password)
105            let salt = generate_salt(); // Still use salt for consistency
106            let nonce = secretbox::gen_nonce();
107            
108            VaultFile {
109                salt,
110                nonce,
111                ciphertext: serialized, // Store data unencrypted
112                created_at: Utc::now(),
113                updated_at: Utc::now(),
114            }
115        };
116
117        self.save_vault_file(&vault_file)?;
118        
119        if password.is_some() {
120            let master_key = MasterKey::from_password(password.unwrap(), &vault_file.salt)?;
121            self.master_key = Some(master_key);
122        }
123        self.data = Some(vault_data);
124
125        Ok(())
126    }
127
128    pub fn is_unlocked(&self) -> bool {
129        self.data.is_some()
130    }
131
132    pub fn add_server(&mut self, server: Server) -> Result<()> {
133        self.ensure_unlocked()?;
134        
135        let data = self.data.as_mut().unwrap();
136        data.add_server(server);
137        
138        self.save()?;
139        Ok(())
140    }
141
142    pub fn remove_server(&mut self, id: &uuid::Uuid) -> Result<bool> {
143        self.ensure_unlocked()?;
144        
145        let data = self.data.as_mut().unwrap();
146        let removed = data.remove_server(id);
147        
148        if removed {
149            self.save()?;
150        }
151        
152        Ok(removed)
153    }
154
155    pub fn list_servers(&self) -> Result<&Vec<Server>> {
156        self.ensure_unlocked()?;
157        
158        Ok(&self.data.as_ref().unwrap().servers)
159    }
160
161    pub fn find_server(&self, id: &uuid::Uuid) -> Result<Option<&Server>> {
162        self.ensure_unlocked()?;
163        
164        Ok(self.data.as_ref().unwrap().find_server(id))
165    }
166
167    pub fn replace_server(&mut self, server: Server) -> Result<bool> {
168        self.ensure_unlocked()?;
169        let data = self.data.as_mut().unwrap();
170        let replaced = data.replace_server(server);
171        if replaced { self.save()?; }
172        Ok(replaced)
173    }
174
175    pub fn vault_path(&self) -> &PathBuf {
176        &self.data_path
177    }
178
179    fn ensure_unlocked(&self) -> Result<()> {
180        if !self.is_unlocked() {
181            return Err(anyhow::anyhow!("Vault is locked"));
182        }
183        Ok(())
184    }
185
186    fn load_vault_file(&self) -> Result<VaultFile> {
187        let content = fs::read(&self.data_path)?;
188        let vault_file: VaultFile = serde_json::from_slice(&content)?;
189        Ok(vault_file)
190    }
191
192    fn save_vault_file(&self, vault_file: &VaultFile) -> Result<()> {
193        let content = serde_json::to_vec(vault_file)?;
194        
195        // Set restrictive permissions before writing
196        let mut file = fs::OpenOptions::new()
197            .create(true)
198            .write(true)
199            .truncate(true)
200            .open(&self.data_path)?;
201            
202        let mut perms = file.metadata()?.permissions();
203        perms.set_mode(0o600); // Read/write for owner only
204        file.set_permissions(perms)?;
205        
206        file.write_all(&content)?;
207        Ok(())
208    }
209
210    fn save(&mut self) -> Result<()> {
211        let data = self.data.as_ref().unwrap();
212        let serialized = serde_json::to_vec(data)?;
213
214        let vault_file = if let Some(master_key) = &self.master_key {
215            // Encrypted vault: reuse existing salt to keep key derivation stable
216            let existing = self.load_vault_file().ok();
217            let salt = existing.as_ref().map(|f| f.salt).unwrap_or_else(generate_salt);
218
219            let (nonce, ciphertext) = master_key.encrypt(&serialized);
220            VaultFile {
221                salt,
222                nonce,
223                ciphertext,
224                created_at: existing.map(|f| f.created_at).unwrap_or_else(|| Utc::now()),
225                updated_at: Utc::now(),
226            }
227        } else {
228            // Unencrypted vault
229            let salt = generate_salt();
230            let nonce = secretbox::gen_nonce();
231            
232            VaultFile {
233                salt,
234                nonce,
235                ciphertext: serialized, // Store unencrypted
236                created_at: self.load_vault_file().map(|f| f.created_at).unwrap_or_else(|_| Utc::now()),
237                updated_at: Utc::now(),
238            }
239        };
240        
241        self.save_vault_file(&vault_file)?;
242        Ok(())
243    }
244}