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 if let Some(password) = password {
64 let master_key = MasterKey::from_password(password, &vault_file.salt)?;
65
66 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 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 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 let salt = generate_salt(); let nonce = secretbox::gen_nonce();
110
111 VaultFile {
112 salt,
113 nonce,
114 ciphertext: serialized, 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); 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 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 let salt = generate_salt();
257 let nonce = secretbox::gen_nonce();
258
259 VaultFile {
260 salt,
261 nonce,
262 ciphertext: serialized, 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}