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 if let Some(password) = password {
61 let master_key = MasterKey::from_password(password, &vault_file.salt)?;
62
63 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 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 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 let salt = generate_salt(); let nonce = secretbox::gen_nonce();
107
108 VaultFile {
109 salt,
110 nonce,
111 ciphertext: serialized, 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 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); 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 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 let salt = generate_salt();
230 let nonce = secretbox::gen_nonce();
231
232 VaultFile {
233 salt,
234 nonce,
235 ciphertext: serialized, 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}