1use super::{
18 vault::{VaultDiskDirectory, VAULT_FILE_NAME},
19 KeyDirectory, VaultKey, VaultKeyDirectory, VaultKeyDirectoryProvider,
20};
21use crate::{
22 json::{self, Uuid},
23 Error, SafeAccount,
24};
25use cfxkey::Password;
26use log::warn;
27use std::{
28 collections::HashMap,
29 fs,
30 io::{self, Write},
31 path::{Path, PathBuf},
32};
33use time;
34
35const IGNORED_FILES: &[&str] = &[
36 "thumbs.db",
37 "address_book.json",
38 "dapps_policy.json",
39 "dapps_accounts.json",
40 "dapps_history.json",
41 "vault.json",
42];
43
44pub fn find_unique_filename_using_random_suffix(
46 parent_path: &Path, original_filename: &str,
47) -> io::Result<String> {
48 let mut path = parent_path.join(original_filename);
49 let mut deduped_filename = original_filename.to_string();
50
51 if path.exists() {
52 const MAX_RETRIES: usize = 500;
53 let mut retries = 0;
54
55 while path.exists() {
56 if retries >= MAX_RETRIES {
57 return Err(io::Error::new(
58 io::ErrorKind::Other,
59 "Exceeded maximum retries when deduplicating filename.",
60 ));
61 }
62
63 let suffix = crate::random::random_string(4);
64 deduped_filename = format!("{}-{}", original_filename, suffix);
65 path.set_file_name(&deduped_filename);
66 retries += 1;
67 }
68 }
69
70 Ok(deduped_filename)
71}
72
73#[cfg(unix)]
76pub fn create_new_file_with_permissions_to_owner(
77 file_path: &Path,
78) -> io::Result<fs::File> {
79 use std::os::unix::fs::OpenOptionsExt;
80
81 fs::OpenOptions::new()
82 .write(true)
83 .create_new(true)
84 .mode((libc::S_IWUSR | libc::S_IRUSR) as u32)
85 .open(file_path)
86}
87
88#[cfg(not(unix))]
91pub fn create_new_file_with_permissions_to_owner(
92 file_path: &Path,
93) -> io::Result<fs::File> {
94 fs::OpenOptions::new()
95 .write(true)
96 .create_new(true)
97 .open(file_path)
98}
99
100#[cfg(unix)]
103pub fn replace_file_with_permissions_to_owner(
104 file_path: &Path,
105) -> io::Result<fs::File> {
106 use std::os::unix::fs::PermissionsExt;
107
108 let file = fs::File::create(file_path)?;
109 let mut permissions = file.metadata()?.permissions();
110 permissions.set_mode((libc::S_IWUSR | libc::S_IRUSR) as u32);
111 file.set_permissions(permissions)?;
112
113 Ok(file)
114}
115
116#[cfg(not(unix))]
119pub fn replace_file_with_permissions_to_owner(
120 file_path: &Path,
121) -> io::Result<fs::File> {
122 fs::File::create(file_path)
123}
124
125pub type RootDiskDirectory = DiskDirectory<DiskKeyFileManager>;
127
128pub trait KeyFileManager: Send + Sync {
130 fn read<T>(
132 &self, filename: Option<String>, reader: T,
133 ) -> Result<SafeAccount, Error>
134 where T: io::Read;
135
136 fn write<T>(
138 &self, account: SafeAccount, writer: &mut T,
139 ) -> Result<(), Error>
140 where T: io::Write;
141}
142
143pub struct DiskDirectory<T>
145where T: KeyFileManager
146{
147 path: PathBuf,
148 key_manager: T,
149}
150
151#[derive(Default)]
153pub struct DiskKeyFileManager {
154 password: Option<Password>,
155}
156
157impl RootDiskDirectory {
158 pub fn create<P>(path: P) -> Result<Self, Error>
159 where P: AsRef<Path> {
160 fs::create_dir_all(&path)?;
161 Ok(Self::at(path))
162 }
163
164 pub fn with_password(&self, password: Option<Password>) -> Self {
167 DiskDirectory::new(&self.path, DiskKeyFileManager { password })
168 }
169
170 pub fn at<P>(path: P) -> Self
171 where P: AsRef<Path> {
172 DiskDirectory::new(path, DiskKeyFileManager::default())
173 }
174}
175
176impl<T> DiskDirectory<T>
177where T: KeyFileManager
178{
179 pub fn new<P>(path: P, key_manager: T) -> Self
181 where P: AsRef<Path> {
182 DiskDirectory {
183 path: path.as_ref().to_path_buf(),
184 key_manager,
185 }
186 }
187
188 fn files(&self) -> Result<Vec<PathBuf>, Error> {
189 Ok(fs::read_dir(&self.path)?
190 .flat_map(Result::ok)
191 .filter(|entry| {
192 let metadata = entry.metadata().ok();
193 let file_name = entry.file_name();
194 let name = file_name.to_string_lossy();
195 metadata.map_or(false, |m| !m.is_dir()) &&
197 !name.starts_with('.') &&
199 !IGNORED_FILES.contains(&&*name)
201 })
202 .map(|entry| entry.path())
203 .collect::<Vec<PathBuf>>())
204 }
205
206 pub fn files_hash(&self) -> Result<u64, Error> {
207 use std::{collections::hash_map::DefaultHasher, hash::Hasher};
208
209 let mut hasher = DefaultHasher::new();
210 let files = self.files()?;
211 for file in files {
212 hasher.write(file.to_str().unwrap_or("").as_bytes())
213 }
214
215 Ok(hasher.finish())
216 }
217
218 fn last_modification_date(&self) -> Result<u64, Error> {
219 use std::time::UNIX_EPOCH;
220 let duration = fs::metadata(&self.path)?
221 .modified()?
222 .duration_since(UNIX_EPOCH)
223 .unwrap_or_default();
224 let timestamp = duration.as_secs() ^ (duration.subsec_nanos() as u64);
225 Ok(timestamp)
226 }
227
228 fn files_content(&self) -> Result<HashMap<PathBuf, SafeAccount>, Error> {
230 let paths = self.files()?;
233 Ok(paths
234 .into_iter()
235 .filter_map(|path| {
236 let filename = Some(
237 path.file_name()
238 .and_then(|n| n.to_str())
239 .expect("Keys have valid UTF8 names only.")
240 .to_owned(),
241 );
242 fs::File::open(path.clone())
243 .map_err(Into::into)
244 .and_then(|file| self.key_manager.read(filename, file))
245 .map_err(|err| {
246 warn!("Invalid key file: {:?} ({})", path, err);
247 err
248 })
249 .map(|account| (path, account))
250 .ok()
251 })
252 .collect())
253 }
254
255 pub fn insert_with_filename(
259 &self, account: SafeAccount, mut filename: String, dedup: bool,
260 ) -> Result<SafeAccount, Error> {
261 if dedup {
262 filename = find_unique_filename_using_random_suffix(
263 &self.path, &filename,
264 )?;
265 }
266
267 let keyfile_path = self.path.join(filename.as_str());
269
270 let original_account = account.clone();
272 let mut account = account;
273 account.filename = Some(filename);
274
275 {
276 let mut file = if dedup {
278 create_new_file_with_permissions_to_owner(&keyfile_path)?
279 } else {
280 replace_file_with_permissions_to_owner(&keyfile_path)?
281 };
282
283 self.key_manager
285 .write(original_account, &mut file)
286 .map_err(|e| Error::Custom(format!("{:?}", e)))?;
287
288 file.flush()?;
289 file.sync_all()?;
290 }
291
292 Ok(account)
293 }
294
295 pub fn key_manager(&self) -> &T { &self.key_manager }
297}
298
299impl<T> KeyDirectory for DiskDirectory<T>
300where T: KeyFileManager
301{
302 fn load(&self) -> Result<Vec<SafeAccount>, Error> {
303 let accounts = self
304 .files_content()?
305 .into_iter()
306 .map(|(_, account)| account)
307 .collect();
308 Ok(accounts)
309 }
310
311 fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
312 let filename = account_filename(&account);
314 self.insert_with_filename(account, filename, false)
315 }
316
317 fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
318 let filename = account_filename(&account);
319 self.insert_with_filename(account, filename, true)
320 }
321
322 fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
323 let to_remove =
326 self.files_content()?.into_iter().find(|&(_, ref acc)| {
327 acc.id == account.id && acc.address == account.address
328 });
329
330 match to_remove {
332 None => Err(Error::InvalidAccount),
333 Some((path, _)) => fs::remove_file(path).map_err(From::from),
334 }
335 }
336
337 fn path(&self) -> Option<&PathBuf> { Some(&self.path) }
338
339 fn as_vault_provider(&self) -> Option<&dyn VaultKeyDirectoryProvider> {
340 Some(self)
341 }
342
343 fn unique_repr(&self) -> Result<u64, Error> {
344 self.last_modification_date()
345 }
346}
347
348impl<T> VaultKeyDirectoryProvider for DiskDirectory<T>
349where T: KeyFileManager
350{
351 fn create(
352 &self, name: &str, key: VaultKey,
353 ) -> Result<Box<dyn VaultKeyDirectory>, Error> {
354 let vault_dir = VaultDiskDirectory::create(&self.path, name, key)?;
355 Ok(Box::new(vault_dir))
356 }
357
358 fn open(
359 &self, name: &str, key: VaultKey,
360 ) -> Result<Box<dyn VaultKeyDirectory>, Error> {
361 let vault_dir = VaultDiskDirectory::at(&self.path, name, key)?;
362 Ok(Box::new(vault_dir))
363 }
364
365 fn list_vaults(&self) -> Result<Vec<String>, Error> {
366 Ok(fs::read_dir(&self.path)?
367 .filter_map(|e| e.ok().map(|e| e.path()))
368 .filter_map(|path| {
369 let mut vault_file_path = path.clone();
370 vault_file_path.push(VAULT_FILE_NAME);
371 if vault_file_path.is_file() {
372 path.file_name()
373 .and_then(|f| f.to_str())
374 .map(|f| f.to_owned())
375 } else {
376 None
377 }
378 })
379 .collect())
380 }
381
382 fn vault_meta(&self, name: &str) -> Result<String, Error> {
383 VaultDiskDirectory::meta_at(&self.path, name)
384 }
385}
386
387impl KeyFileManager for DiskKeyFileManager {
388 fn read<T>(
389 &self, filename: Option<String>, reader: T,
390 ) -> Result<SafeAccount, Error>
391 where T: io::Read {
392 let key_file = json::KeyFile::load(reader)
393 .map_err(|e| Error::Custom(format!("{:?}", e)))?;
394 SafeAccount::from_file(key_file, filename, &self.password)
395 }
396
397 fn write<T>(
398 &self, mut account: SafeAccount, writer: &mut T,
399 ) -> Result<(), Error>
400 where T: io::Write {
401 account.meta = json::remove_vault_name_from_json_meta(&account.meta)
404 .map_err(|err| Error::Custom(format!("{:?}", err)))?;
405
406 let key_file: json::KeyFile = account.into();
407 key_file
408 .write(writer)
409 .map_err(|e| Error::Custom(format!("{:?}", e)))
410 }
411}
412
413fn account_filename(account: &SafeAccount) -> String {
414 account.filename.clone().unwrap_or_else(|| {
416 let time_format = time::macros::format_description!(
417 "[year]-[month]-[day]T[hour]-[minute]-[second]"
418 );
419 let timestamp = time::OffsetDateTime::now_utc()
420 .format(&time_format)
421 .expect("Time-format string is valid.");
422 format!("UTC--{}Z--{}", timestamp, Uuid::from(account.id))
423 })
424}
425
426#[cfg(test)]
427mod test {
428 use super::{KeyDirectory, RootDiskDirectory, VaultKey};
429 use crate::account::SafeAccount;
430 use cfxkey::{Generator, Random};
431 use std::{env, fs};
432 use tempfile::tempdir;
433
434 #[test]
435 fn should_create_new_account() {
436 let mut dir = env::temp_dir();
438 dir.push("cfxstore_should_create_new_account");
439 let keypair = Random.generate().unwrap();
440 let password = "hello world".into();
441 let directory = RootDiskDirectory::create(dir.clone()).unwrap();
442
443 let account = SafeAccount::create(
445 &keypair,
446 [0u8; 16],
447 &password,
448 1024,
449 "Test".to_owned(),
450 "{}".to_owned(),
451 );
452 let res = directory.insert(account.unwrap());
453
454 assert!(res.is_ok(), "Should save account succesfuly.");
456 assert!(
457 res.unwrap().filename.is_some(),
458 "Filename has been assigned."
459 );
460
461 let _ = fs::remove_dir_all(dir);
463 }
464
465 #[test]
466 fn should_handle_duplicate_filenames() {
467 let mut dir = env::temp_dir();
469 dir.push("cfxstore_should_handle_duplicate_filenames");
470 let keypair = Random.generate().unwrap();
471 let password = "hello world".into();
472 let directory = RootDiskDirectory::create(dir.clone()).unwrap();
473
474 let account = SafeAccount::create(
476 &keypair,
477 [0u8; 16],
478 &password,
479 1024,
480 "Test".to_owned(),
481 "{}".to_owned(),
482 )
483 .unwrap();
484 let filename = "test".to_string();
485 let dedup = true;
486
487 directory
488 .insert_with_filename(account.clone(), "foo".to_string(), dedup)
489 .unwrap();
490 let file1 = directory
491 .insert_with_filename(account.clone(), filename.clone(), dedup)
492 .unwrap()
493 .filename
494 .unwrap();
495 let file2 = directory
496 .insert_with_filename(account.clone(), filename.clone(), dedup)
497 .unwrap()
498 .filename
499 .unwrap();
500 let file3 = directory
501 .insert_with_filename(account, filename.clone(), dedup)
502 .unwrap()
503 .filename
504 .unwrap();
505
506 assert_eq!(file1, filename);
509
510 assert!(file2 != file3);
512 assert_eq!(file2.len(), filename.len() + 5);
513 assert_eq!(file3.len(), filename.len() + 5);
514
515 let _ = fs::remove_dir_all(dir);
517 }
518
519 #[test]
520 fn should_manage_vaults() {
521 let mut dir = env::temp_dir();
523 dir.push("should_create_new_vault");
524 let directory = RootDiskDirectory::create(dir.clone()).unwrap();
525 let vault_name = "vault";
526 let password = "password".into();
527
528 assert!(directory.as_vault_provider().is_some());
530
531 let before_root_items_count = fs::read_dir(&dir).unwrap().count();
533 let vault = directory
534 .as_vault_provider()
535 .unwrap()
536 .create(vault_name, VaultKey::new(&password, 1024));
537
538 assert!(vault.is_ok());
540 let after_root_items_count = fs::read_dir(&dir).unwrap().count();
541 assert!(after_root_items_count > before_root_items_count);
542
543 let vault = directory
545 .as_vault_provider()
546 .unwrap()
547 .open(vault_name, VaultKey::new(&password, 1024));
548
549 assert!(vault.is_ok());
551 let after_root_items_count2 = fs::read_dir(&dir).unwrap().count();
552 assert!(after_root_items_count == after_root_items_count2);
553
554 let _ = fs::remove_dir_all(dir);
556 }
557
558 #[test]
559 fn should_list_vaults() {
560 let temp_path = tempdir().unwrap();
562 let directory = RootDiskDirectory::create(&temp_path).unwrap();
563 let vault_provider = directory.as_vault_provider().unwrap();
564 vault_provider
565 .create("vault1", VaultKey::new(&"password1".into(), 1))
566 .unwrap();
567 vault_provider
568 .create("vault2", VaultKey::new(&"password2".into(), 1))
569 .unwrap();
570
571 let vaults = vault_provider.list_vaults().unwrap();
573 assert_eq!(vaults.len(), 2);
574 assert!(vaults.iter().any(|v| &*v == "vault1"));
575 assert!(vaults.iter().any(|v| &*v == "vault2"));
576 }
577
578 #[test]
579 fn hash_of_files() {
580 let temp_path = tempdir().unwrap();
581 let directory = RootDiskDirectory::create(&temp_path).unwrap();
582
583 let hash = directory
584 .files_hash()
585 .expect("Files hash should be calculated ok");
586 assert_eq!(hash, 15_130_871_412_783_076_140);
587
588 let keypair = Random.generate().unwrap();
589 let password = "test pass".into();
590 let account = SafeAccount::create(
591 &keypair,
592 [0u8; 16],
593 &password,
594 1024,
595 "Test".to_owned(),
596 "{}".to_owned(),
597 );
598 directory
599 .insert(account.unwrap())
600 .expect("Account should be inserted ok");
601
602 let new_hash = directory
603 .files_hash()
604 .expect("New files hash should be calculated ok");
605
606 assert!(new_hash != hash, "hash of the file list should change once directory content changed");
607 }
608}