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