Skip to main content

portkey/
vault.rs

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