1extern crate async_trait;
24extern crate base64;
25extern crate dirs;
26extern crate futures;
27extern crate http;
28extern crate hyper;
29extern crate hyper_tls;
30extern crate log;
31extern crate native_tls;
32extern crate openssl_probe;
33extern crate rand;
34extern crate rs_password_utils;
35extern crate secstr;
36extern crate sha3;
37extern crate terminal_clipboard;
38extern crate toml;
39extern crate xml;
40
41use std::path::PathBuf;
42use std::time::Duration;
43
44pub use api::GeneralConfiguration;
45use async_trait::async_trait;
46use asynch::{AsyncTask, SyncStatus};
47use futures::{future, select, FutureExt};
48use log::*;
49
50pub use file_handler::default_rustkeylock_location;
51use rest_server::RestService;
52use tokio::time::sleep;
53
54use crate::api::{EditorShowMessageWrapper, PasswordChecker, RklPasswordChecker};
55use crate::asynch::dropbox::DropboxConfiguration;
56use crate::asynch::nextcloud::NextcloudConfiguration;
57use crate::datacrypt::BcryptAes;
58
59use self::api::safe::Safe;
60
61include!(concat!(env!("OUT_DIR"), "/rkl_version.rs"));
63
64pub use self::api::{
65 AllConfigurations, Entry, EntryMeta, EntryPresentationType, Menu, MessageSeverity, UserOption,
66 UserSelection,
67};
68use self::api::{Props, RklConfiguration, RklContent, SystemConfiguration};
69pub use self::asynch::dropbox;
70pub use self::asynch::nextcloud;
71
72mod api;
73mod asynch;
74mod datacrypt;
75mod errors;
76mod file_handler;
77mod protected;
78mod rest_server;
79mod utils;
80
81const FILENAME: &str = ".sec";
82const PROPS_FILENAME: &str = ".props";
83const BCRYPT_COST: u32 = 12;
84const BCRYPT_COST_PRE_0_17_0: u32 = 7;
85
86pub async fn execute_async(editor: Box<dyn AsyncEditor>) {
89
90 let mut rest_server = RestService::new(editor.start_rest_server())
91 .await
92 .expect("Could not start the rest server");
93 let rest_server_clone = rest_server.clone();
94
95 if editor.start_rest_server() {
96 tokio::task::spawn(async move {
97 loop {
98 if let Err(e) = rest_server.serve().await {
99 error!("Could not serve HTTP Rest servers: {e}");
100 }
101 }
102 });
103 }
104
105 unsafe {
106 openssl_probe::init_openssl_env_vars();
107 }
108 info!("Starting rust-keylock...");
109 let props = match file_handler::load_properties(PROPS_FILENAME) {
110 Ok(m) => m,
111 Err(error) => {
112 error!(
113 "Could not load properties. Using defaults. The error was: {}",
114 error
115 );
116 Props::default()
117 }
118 };
119
120 let mut executor = CoreLogicHandler::new(editor, props).await;
121
122 loop {
123 if let Err(e) = rest_server_clone.update_safe(executor.get_safe()) {
124 error!("Could not update the safe for the HTTP server: {e}");
125 }
126 let token = executor
127 .get_configuration()
128 .general
129 .browser_extension_token
130 .unwrap_or_default();
131 if let Err(e) = rest_server_clone.update_token(token.to_string()) {
132 error!("Could not update the safe for the HTTP server: {e}");
133 }
134 let (new_executor, stop) = executor.handle().await.unwrap();
135 executor = new_executor;
136 if stop {
137 break;
138 }
139 }
140
141 info!("Exiting rust-keylock...");
142}
143
144struct CoreLogicHandler {
145 props: Props,
146 user_selection: UserSelection,
148 safe: Safe,
150 configuration: RklConfiguration,
151 dbx_synchronizer: dropbox::Synchronizer,
152 nc_synchronizer: nextcloud::Synchronizer,
153 contents_changed: bool,
155 cryptor: datacrypt::BcryptAes,
156 editor: Box<dyn AsyncEditor>
157}
158
159impl CoreLogicHandler {
160 async fn new(editor: Box<dyn AsyncEditor>, props: Props) -> CoreLogicHandler {
161 let mut props = props;
162 let editor = editor;
163 let user_selection;
165
166 let mut safe = Safe::new();
168 let mut configuration = RklConfiguration::from((
170 nextcloud::NextcloudConfiguration::default(),
171 dropbox::DropboxConfiguration::default(),
172 SystemConfiguration::default(),
173 GeneralConfiguration::default(),
174 ));
175 let contents_changed = false;
177 let cryptor: BcryptAes;
179 loop {
180 let provided_password = if file_handler::is_first_run(FILENAME) {
182 editor.show_change_password().await
183 } else {
184 editor.show_password_enter().await
185 };
186
187 let (us, cr) = handle_provided_password_for_init(
189 provided_password,
190 FILENAME,
191 &mut safe,
192 &mut configuration,
193 &editor,
194 ).await;
195 if us != UserSelection::GoTo(Menu::TryPass(false)) {
197 props.set_version(rkl_version());
199 let _ = file_handler::save_props(&props, PROPS_FILENAME);
200 user_selection = us;
202 cryptor = cr;
203 break;
204 }
205 };
206
207 let mut nc_synchronizer = nextcloud::Synchronizer::new(
209 &configuration.nextcloud,
210 &configuration.system,
211 FILENAME
212 ).unwrap();
213 let _ = nc_synchronizer.init().await;
214
215 let mut dbx_synchronizer = dropbox::Synchronizer::new(
216 &configuration.dropbox,
217 &configuration.system,
218 FILENAME
219 ).unwrap();
220 let _ = dbx_synchronizer.init().await;
221
222 CoreLogicHandler {
223 editor,
224 props: props,
225 user_selection,
226 safe,
227 configuration,
228 dbx_synchronizer,
229 nc_synchronizer,
230 contents_changed,
231 cryptor,
232 }
233 }
234
235 pub(crate) fn get_safe(&self) -> Safe {
236 self.safe.clone()
237 }
238
239 pub(crate) fn get_configuration(&self) -> RklConfiguration {
240 self.configuration.clone()
241 }
242
243 #[allow(clippy::cyclomatic_complexity)]
246 async fn handle(self) -> errors::Result<(CoreLogicHandler, bool)> {
247 let mut stop = false;
248 let mut s = self;
249
250 s.editor.sort_entries(&mut s.safe.entries);
251 let user_selection_future = match s.user_selection {
253 UserSelection::GoTo(Menu::TryPass(update_last_sync_version)) => {
254 let (user_selection, cr) = handle_provided_password_for_init(
255 s.editor.show_password_enter().await,
256 FILENAME,
257 &mut s.safe,
258 &mut s.configuration,
259 &s.editor,
260 ).await;
261 if update_last_sync_version {
262 s.configuration.update_system_last_sync();
263 let rkl_content = RklContent::from((
264 &s.safe,
265 &s.configuration.nextcloud,
266 &s.configuration.dropbox,
267 &s.configuration.system,
268 &s.configuration.general,
269 ));
270 let _ =
271 rkl_content.and_then(|c| file_handler::save(c, FILENAME, &s.cryptor, true));
272 }
273 s.cryptor = cr;
274 Box::pin(future::ready(user_selection))
275 }
276 UserSelection::GoTo(Menu::Main) => {
277 debug!("UserSelection::GoTo(Menu::Main)");
278 s.editor.show_menu(Menu::Main)
279 }
280 UserSelection::GoTo(Menu::ChangePass) => {
281 debug!("UserSelection::GoTo(Menu::ChangePass)");
282 s.contents_changed = true;
283 s.editor.show_change_password()
284 }
285 UserSelection::ProvidedPassword(pwd, salt_pos) => {
286 debug!("UserSelection::GoTo(Menu::ProvidedPassword)");
287 s.cryptor =
288 file_handler::create_bcryptor(FILENAME, pwd.to_string(), bcrypt_cost_from_file(), *salt_pos, true, true)
289 .unwrap();
290 Box::pin(future::ready(UserSelection::GoTo(Menu::Main)))
291 }
292 UserSelection::GoTo(Menu::EntriesList(filter_opt)) => {
293 match filter_opt {
294 Some(filter) => {
295 debug!(
296 "UserSelection::GoTo(Menu::EntriesList) with filter '{}'",
297 &filter
298 );
299 s.safe.set_filter(filter.clone());
300 },
301 None => {
302 debug!(
303 "UserSelection::GoTo(Menu::EntriesList) with existing filter '{}'",
304 &s.safe.get_filter()
305 );
306 }
307 }
308 s.editor.show_entries(s.safe.get_entries().to_vec(), s.safe.get_filter())
309 }
310 UserSelection::GoTo(Menu::NewEntry(opt)) => {
311 debug!("UserSelection::GoTo(Menu::NewEntry)");
312 s.editor.show_menu(Menu::NewEntry(opt))
313 }
314 UserSelection::GoTo(Menu::ShowEntry(index)) => {
315 debug!("UserSelection::GoTo(Menu::ShowEntry(index))");
316 s.editor.show_entry(
317 s.safe.get_entry_decrypted(index),
318 index,
319 EntryPresentationType::View,
320 )
321 }
322 UserSelection::GoTo(Menu::EditEntry(index)) => {
323 debug!("UserSelection::GoTo(Menu::EditEntry(index))");
324 s.editor.show_entry(
325 s.safe.get_entry_decrypted(index),
326 index,
327 EntryPresentationType::Edit,
328 )
329 }
330 UserSelection::GoTo(Menu::DeleteEntry(index)) => {
331 debug!("UserSelection::GoTo(Menu::DeleteEntry(index))");
332 s.editor.show_entry(
333 s.safe.get_entry_decrypted(index),
334 index,
335 EntryPresentationType::Delete,
336 )
337 }
338 UserSelection::GoTo(Menu::Save(update_last_sync_version)) => {
339 debug!("UserSelection::GoTo(Menu::Save({}))",update_last_sync_version);
340 if s.configuration.nextcloud.is_filled() && s.configuration.dropbox.is_filled() {
341 error!("Cannot save because both Nextcloud and Dropbox are configured");
342 s.editor.show_message("Having both Nextcloud and Dropbox configured may lead to unexpected state and currently is not allowed.\
343 Please configure only one of them.", vec![UserOption::ok()], MessageSeverity::Error).await;
344 Box::pin(future::ready(UserSelection::GoTo(Menu::Current)))
345 } else {
346 let _ = s
347 .configuration
348 .update_system_for_save(update_last_sync_version)
349 .map_err(|error| error!("Cannot update system for save: {:?}", error));
350 s.safe.set_filter("".to_string());
352 let mut nc_synchronizer = nextcloud::Synchronizer::new(
354 &s.configuration.nextcloud,
355 &s.configuration.system,
356 FILENAME
357 ).unwrap();
358 let started = nc_synchronizer.init().await;
359 if started.is_err() {
360 let _ = s.editor.show_message(
361 "Could not start the Nextcloud synchronizer",
362 vec![UserOption::ok()],
363 MessageSeverity::Error,
364 ).await;
365 }
366 s.nc_synchronizer = nc_synchronizer;
367
368 let mut dbx_synchronizer = dropbox::Synchronizer::new(
369 &s.configuration.dropbox,
370 &s.configuration.system,
371 FILENAME
372 ).unwrap();
373 let started = dbx_synchronizer.init().await;
374 if started.is_err() {
375 let _ = s.editor.show_message(
376 "Could not start the Dropbox synchronizer",
377 vec![UserOption::ok()],
378 MessageSeverity::Error,
379 ).await;
380 }
381 s.dbx_synchronizer = dbx_synchronizer;
382
383 let rkl_content = RklContent::from((
384 &s.safe,
385 &s.configuration.nextcloud,
386 &s.configuration.dropbox,
387 &s.configuration.system,
388 &s.configuration.general,
389 ));
390 let res =
391 rkl_content.and_then(|c| file_handler::save(c, FILENAME, &s.cryptor, true));
392 match res {
393 Ok(_) => {
394 s.contents_changed = false;
396 if !update_last_sync_version {
397 let _ = s.editor.show_message(
398 "Encrypted and saved successfully!",
399 vec![UserOption::ok()],
400 MessageSeverity::default(),
401 ).await;
402 }
403 }
404 Err(error) => {
405 let _ = s.editor.show_message(
406 "Could not save...",
407 vec![UserOption::ok()],
408 MessageSeverity::Error,
409 ).await;
410 error!("Could not save... {:?}", error);
411 }
412 };
413 if update_last_sync_version {
414 Box::pin(future::ready(UserSelection::GoTo(Menu::Current)))
415 } else {
416 Box::pin(future::ready(UserSelection::GoTo(Menu::Main)))
417 }
418 }
419 }
420 UserSelection::GoTo(Menu::Exit) => {
421 debug!("UserSelection::GoTo(Menu::Exit)");
422 s.editor.exit(s.contents_changed)
423 }
424 UserSelection::GoTo(Menu::ForceExit) => {
425 debug!("UserSelection::GoTo(Menu::ForceExit)");
426 stop = true;
427 Box::pin(future::ready(UserSelection::GoTo(Menu::Current)))
428 }
429 UserSelection::NewEntry(mut entry) => {
430 debug!("UserSelection::NewEntry(entry)");
431
432 let entry_to_replace_opt = match RklPasswordChecker::default()
433 .is_unsafe(&entry.pass)
434 .await
435 {
436 Ok(true) => {
437 warn!("The password for entry {} has leaked!", entry.name);
438 let sel = s.editor.show_message(
439 "The password you provided has been leaked and is not safe. Are you sure you want to use it?",
440 vec![UserOption::yes(), UserOption::no()],
441 MessageSeverity::Warn).await;
442
443 if sel == UserSelection::UserOption(UserOption::yes()) {
444 warn!(
445 "The user accepted that entry {} will have leaked password.",
446 entry.name
447 );
448 entry.meta.leaked_password = true;
449 Some(entry)
450 } else {
451 None
452 }
453 }
454 Ok(false) => {
455 debug!("The password for entry {} is not leaked!", entry.name);
456 entry.meta.leaked_password = false;
457 Some(entry)
458 }
459 Err(error) => {
460 debug!(
461 "No information about password leakage for entry {}. Reason: {}",
462 entry.name, error
463 );
464 Some(entry)
465 }
466 };
467
468 if let Some(entry) = entry_to_replace_opt {
469 s.safe.add_entry(entry);
470 s.contents_changed = true;
471 Box::pin(future::ready(UserSelection::GoTo(Menu::EntriesList(Some("".to_string())))))
472 } else {
473 Box::pin(future::ready(UserSelection::GoTo(Menu::Current)))
474 }
475 }
476 UserSelection::ReplaceEntry(index, mut entry) => {
477 debug!("UserSelection::ReplaceEntry(index, entry)");
478
479 let entry_to_replace_opt = match RklPasswordChecker::default()
480 .is_unsafe(&entry.pass)
481 .await
482 {
483 Ok(true) => {
484 warn!("The password for entry {} has leaked!", entry.name);
485 let sel = s.editor.show_message(
486 "The password you provided has been leaked and is not safe. Are you sure you want to use it?",
487 vec![UserOption::yes(), UserOption::no()],
488 MessageSeverity::Warn).await;
489
490 if sel == UserSelection::UserOption(UserOption::yes()) {
491 warn!(
492 "The user accepted that entry {} will have leaked password.",
493 entry.name
494 );
495 entry.meta.leaked_password = true;
496 Some(entry)
497 } else {
498 None
499 }
500 }
501 Ok(false) => {
502 debug!("The password for entry {} is not leaked!", entry.name);
503 entry.meta.leaked_password = false;
504 Some(entry)
505 }
506 Err(error) => {
507 debug!(
508 "No information about password leakage for entry {}. Reason: {}",
509 entry.name, error
510 );
511 Some(entry)
512 }
513 };
514
515 if let Some(entry) = entry_to_replace_opt {
516 s.contents_changed = true;
517 match s.safe.replace_entry(index, entry) {
518 Ok(_) => { }
519 Err(error) => {
520 error!("Could not replace entry: {:?}", error);
521 let _ = s.editor.show_message("Could not replace the password entry. Please see the logs for more details.", vec![UserOption::ok()], MessageSeverity::Error).await;
522 }
523 }
524 Box::pin(future::ready(UserSelection::GoTo(Menu::EntriesList(None))))
525 } else {
526 Box::pin(future::ready(UserSelection::GoTo(Menu::Current)))
527 }
528 }
529 UserSelection::DeleteEntry(index) => {
530 debug!("UserSelection::DeleteEntry(index)");
531 let _ = s.safe.remove_entry(index).map_err(|err| {
532 error!("Could not delete entry {:?}", err);
533 let _ = s.editor.show_message(
534 "Could not delete entry. Please see the logs for more details.",
535 vec![UserOption::ok()],
536 MessageSeverity::Error,
537 );
538 });
539 s.contents_changed = true;
540 Box::pin(future::ready(UserSelection::GoTo(Menu::EntriesList(None))))
541 }
542 UserSelection::GoTo(Menu::TryFileRecovery) => {
543 debug!("UserSelection::GoTo(Menu::TryFileRecovery)");
544 let _ = s.editor.show_message(
545 "The password entries are corrupted.\n\nPress Enter to attempt recovery...",
546 vec![UserOption::ok()],
547 MessageSeverity::Error,
548 ).await;
549 let mut rec_entries = match file_handler::recover(FILENAME, &s.cryptor) {
550 Ok(recovered_entries) => {
551 let message = r#"
552Recovery succeeded...
553
554Note the errors that caused the recovery. You may see some useful information about possible values that could not be recovered.
555Press Enter to show the Recovered Entries and if you are ok with it, save them.
556
557Warning: Saving will discard all the entries that could not be recovered.
558"#;
559 let _ = s.editor.show_message(
560 message,
561 vec![UserOption::ok()],
562 MessageSeverity::default(),
563 ).await;
564 s.contents_changed = true;
565 s.safe.entries.clear();
566 recovered_entries
567 }
568 Err(error) => {
569 let message = format!("Recovery failed... Reason {:?}", error);
570 error!("{}", &message);
571 let _ = s.editor.show_message(
572 "Recovery failed...",
573 vec![UserOption::ok()],
574 MessageSeverity::Error,
575 ).await;
576 s.safe.entries.clone()
577 }
578 };
579 s.safe.entries.append(&mut rec_entries);
580
581 Box::pin(future::ready(UserSelection::GoTo(Menu::EntriesList(Some("".to_string())))))
582 }
583 UserSelection::GoTo(Menu::ExportEntries) => {
584 debug!("UserSelection::GoTo(Menu::ExportEntries)");
585 s.editor.show_menu(Menu::ExportEntries)
586 }
587 UserSelection::ExportTo(path) => {
588 debug!("UserSelection::ExportTo(path)");
589 let do_export = if file_handler::file_exists(&PathBuf::from(&path)) {
590 let selection = s.editor.show_message(
591 "This will overwrite an existing file. Do you want to proceed?",
592 vec![UserOption::yes(), UserOption::no()],
593 MessageSeverity::Warn,
594 ).await;
595
596 debug!(
597 "The user selected {:?} as an answer for overwriting the file {}",
598 selection, path
599 );
600 selection == UserSelection::UserOption(UserOption::yes())
601 } else {
602 true
603 };
604
605 if do_export {
606 match RklContent::from((
607 &s.safe,
608 &s.configuration.nextcloud,
609 &s.configuration.dropbox,
610 &s.configuration.system,
611 &s.configuration.general,
612 )) {
613 Ok(c) => match file_handler::save(c, &path, &s.cryptor, false) {
614 Ok(_) => {
615 let _ = s.editor.show_message(
616 "Export completed successfully!",
617 vec![UserOption::ok()],
618 MessageSeverity::default(),
619 ).await;
620 }
621 Err(error) => {
622 error!("Could not export... {:?}", error);
623 let _ = s.editor.show_message(
624 "Could not export...",
625 vec![UserOption::ok()],
626 MessageSeverity::Error,
627 ).await;
628 }
629 },
630 Err(error) => {
631 error!("Could not export... {:?}", error);
632 let _ = s.editor.show_message(
633 "Could not export...",
634 vec![UserOption::ok()],
635 MessageSeverity::Error,
636 ).await;
637 }
638 };
639 Box::pin(future::ready(UserSelection::GoTo(Menu::Main)))
640 } else {
641 Box::pin(future::ready(UserSelection::GoTo(Menu::ExportEntries)))
642 }
643 }
644 UserSelection::GoTo(Menu::ImportEntries) => {
645 debug!("UserSelection::GoTo(Menu::ImportEntries)");
646 s.editor.show_menu(Menu::ImportEntries)
647 }
648 us @ UserSelection::ImportFrom(_, _, _)
649 | us @ UserSelection::ImportFromDefaultLocation(_, _, _) => {
650 let import_from_default_location = match us {
651 UserSelection::ImportFrom(_, _, _) => false,
652 UserSelection::ImportFromDefaultLocation(_, _, _) => true,
653 _ => false,
654 };
655 match us {
656 UserSelection::ImportFrom(path, pwd, salt_pos)
657 | UserSelection::ImportFromDefaultLocation(path, pwd, salt_pos) => {
658 let cr = file_handler::create_bcryptor(
659 &path,
660 pwd.to_string(),
661 bcrypt_cost_from_file(),
662 *salt_pos,
663 false,
664 import_from_default_location,
665 )
666 .unwrap();
667 debug!("UserSelection::ImportFrom(path, pwd, salt_pos)");
668
669 match file_handler::load(&path, &cr, import_from_default_location) {
670 Err(error) => {
671 error!("Could not import... {:?}", error);
672 let _ = s.editor.show_message(
673 "Could not import...",
674 vec![UserOption::ok()],
675 MessageSeverity::Error,
676 ).await;
677 }
678 Ok(rkl_content) => {
679 let message =
680 format!("Imported {} entries!", &rkl_content.entries.len());
681 debug!("{}", message);
682 s.contents_changed = true;
684 s.safe.merge(rkl_content.entries);
686 s.configuration.system = rkl_content.system_conf;
688 s.configuration.update_system_last_sync();
690
691 let _ = s.editor.show_message(
692 &message,
693 vec![UserOption::ok()],
694 MessageSeverity::default(),
695 ).await;
696 }
697 };
698 }
699 _ => {}
700 };
701
702 Box::pin(future::ready(UserSelection::GoTo(Menu::Main)))
703 }
704 UserSelection::GoTo(Menu::ShowConfiguration) => {
705 debug!("UserSelection::GoTo(Menu::ShowConfiguration)");
706 s.editor.show_configuration(
707 s.configuration.nextcloud.clone(),
708 s.configuration.dropbox.clone(),
709 s.configuration.general.clone(),
710 )
711 }
712 UserSelection::GenerateBrowserExtensionToken => {
713 debug!("UserSelection::GenerateBrowserExtensionToken");
714 let new_token = rs_password_utils::dice::generate_with_separator(
715 s.props.generated_passphrases_words_count() as usize,
716 "_",
717 );
718 let mut updated_gen_conf = s.configuration.general.clone();
719 updated_gen_conf.browser_extension_token = Some(new_token);
720 s.editor.show_configuration(
721 s.configuration.nextcloud.clone(),
722 s.configuration.dropbox.clone(),
723 updated_gen_conf,
724 )
725 }
726 UserSelection::UpdateConfiguration(new_conf) => {
727 debug!("UserSelection::UpdateConfiguration");
728 if new_conf.nextcloud.is_filled() && new_conf.dropbox.is_filled() {
729 error!("Cannot update the configuration because both Nextcloud and Dropbox are configured");
730 s.editor.show_message("Having both Nextcloud and Dropbox configured may lead to unexpected state and currently is not allowed.\
731 Please configure only one of them.", vec![UserOption::ok()], MessageSeverity::Error).await;
732 Box::pin(future::ready(UserSelection::GoTo(Menu::Current)))
733 } else {
734 s.configuration.nextcloud = new_conf.nextcloud;
735 s.configuration.dropbox = new_conf.dropbox;
736 if s.configuration.general != new_conf.general {
737 debug!("General configuration changed");
738 s.contents_changed = true;
739 s.configuration.general = new_conf.general;
740 }
741 if s.configuration.nextcloud.is_filled() {
742 debug!("A valid configuration for Nextcloud synchronization was found after being updated by the User. Spawning \
743 nextcloud sync task");
744 s.contents_changed = true;
745 let stopped = s.nc_synchronizer.stop();
747 if stopped.is_err() {
748 s.editor.show_message("Could not stop the nextcloud synchronizer.", vec![UserOption::ok()], MessageSeverity::Error).await;
749 }
750 }
751 if s.configuration.dropbox.is_filled() {
752 debug!("A valid configuration for dropbox synchronization was found after being updated by the User. Spawning \
753 dropbox sync task");
754 s.contents_changed = true;
755 let stopped = s.dbx_synchronizer.stop();
757 if stopped.is_err() {
758 s.editor.show_message("Could not stop the Dropbox synchronizer.", vec![UserOption::ok()], MessageSeverity::Error).await;
759 }
760 }
761 Box::pin(future::ready(UserSelection::GoTo(Menu::Main)))
762 }
763 }
764 UserSelection::AddToClipboard(content) => {
765 debug!("UserSelection::AddToClipboard");
766 let res = terminal_clipboard::set_string(content).map_err(|error| {
767 errors::RustKeylockError::GeneralError(error.to_string())
768 });
769 match res {
770 Ok(_) => {
771 let _ = s.editor.show_message("Copied! ", vec![UserOption::ok()], MessageSeverity::default()).await;
772 }
773 Err(error) => {
774 error!("Could not copy: {:?}", error);
775 let error_message = format!("Could not copy... Reason: {}", error);
776 let _ = s.editor.show_message(&error_message, vec![UserOption::ok()], MessageSeverity::Error).await;
777 }
778 };
779
780 Box::pin(future::ready(UserSelection::GoTo(Menu::Current)))
782 }
783 UserSelection::GoTo(Menu::WaitForDbxTokenCallback(url)) => {
784 debug!("UserSelection::GoTo(Menu::WaitForDbxTokenCallback)");
785 match dropbox::retrieve_token(url).await {
786 Ok(token) => {
787 if token.is_empty() {
788 let _ = s.editor.show_message(
789 "Empty Dropbox Authentication token was retrieved.",
790 vec![UserOption::ok()],
791 MessageSeverity::Error,
792 );
793 Box::pin(future::ready(UserSelection::GoTo(Menu::ShowConfiguration)))
794 } else {
795 Box::pin(future::ready(UserSelection::GoTo(Menu::SetDbxToken(token))))
796 }
797 }
798 Err(error) => {
799 error!(
800 "Error while retrieving Dropbox Authentication token: {} ({:?})",
801 error, error
802 );
803 let _ = s.editor.show_message(
804 &format!(
805 "Error while retrieving Dropbox Authentication token: {}",
806 error
807 ),
808 vec![UserOption::ok()],
809 MessageSeverity::Error,
810 ).await;
811 Box::pin(future::ready(UserSelection::GoTo(Menu::ShowConfiguration)))
812 }
813 }
814 }
815 UserSelection::GoTo(Menu::SetDbxToken(token)) => {
816 debug!("UserSelection::GoTo(Menu::SetDbxToken)");
817 let tok_res = dropbox::DropboxConfiguration::new(token);
818 match tok_res {
819 Ok(dbx_conf) => {
820 s.contents_changed = true;
821 s.configuration.dropbox = dbx_conf;
822 Box::pin(future::ready(UserSelection::GoTo(Menu::ShowConfiguration)))
823 }
824 Err(error) => {
825 error!("Could not set the Dropbox token: {:?}", error);
826 let _ = s.editor.show_message("Could not obtain the Dropbox token. Please see the logs for more details.", vec![UserOption::ok()], MessageSeverity::Error);
827 Box::pin(future::ready(UserSelection::GoTo(Menu::ShowConfiguration)))
828 }
829 }
830 }
831 UserSelection::GeneratePassphrase(index_opt, mut entry) => {
832 debug!("UserSelection::GoTo(Menu::GeneratePassphrase)");
833 entry.pass = rs_password_utils::dice::generate_with_separator(
834 s.props.generated_passphrases_words_count() as usize,
835 "_"
836 );
837 match index_opt {
838 Some(index) => s
839 .editor
840 .show_entry(entry, index, EntryPresentationType::Edit),
841 None => {
842 s.editor.show_menu(Menu::NewEntry(Some(entry)))
843 },
844 }
845 }
846 UserSelection::CheckPasswords => {
847 debug!("UserSelection::CheckPasswords");
848 match handle_check_passwords(&mut s.safe, &RklPasswordChecker::default()).await {
849 Ok(mr) => {
850 let _ = s
851 .editor
852 .show_message(&mr.message, mr.user_options, mr.severity).await;
853 Box::pin(future::ready(UserSelection::GoTo(Menu::EntriesList(Some("".to_string())))))
854 }
855 Err(error) => {
856 let _ = s.editor.show_message(
857 error.to_string().as_str(),
858 vec![UserOption::ok()],
859 MessageSeverity::Error,
860 ).await;
861 Box::pin(future::ready(UserSelection::GoTo(Menu::EntriesList(Some("".to_string())))))
862 }
863 }
864 }
865 UserSelection::GoTo(Menu::Current) => {
866 debug!("UserSelection::GoTo(Menu::Current)");
867 s.editor.show_menu(Menu::Current)
868 }
869 other => {
870 let message = format!("Bug: User Selection '{:?}' should not be handled in the main loop. Please, consider opening a bug \
871 to the developers.",
872 &other);
873 error!("{}", message);
874 panic!("{}", message)
875 }
876 };
877
878 let nc_synchronizer_clonne = s.nc_synchronizer.clone();
880 let dbx_synchronizer_clone = s.dbx_synchronizer.clone();
881 let mut nc_future = nc_synchronizer_clonne.execute().fuse();
882 let mut dbx_future = dbx_synchronizer_clone.execute().fuse();
883 let mut fused_user_selection_future = user_selection_future.fuse();
884 let mut inactivity_timeout_future = Box::pin(sleep(Duration::from_secs(s.props.idle_timeout_seconds() as u64))).fuse();
885
886 let mut loop_result;
887 loop {
888 loop_result = select! {
890 selection_from_future = fused_user_selection_future => {
891 LoopResult::LoopUserSelection(selection_from_future)
892 },
893 sync_status_res = nc_future => {
894 if sync_status_res == Ok(SyncStatus::None) {
895 LoopResult::Ignore("nextcloud")
896 } else {
897 LoopResult::LoopSyncStatus(sync_status_res, "nextcloud")
898 }
899 },
900 sync_status_res = dbx_future => {
901 if sync_status_res == Ok(SyncStatus::None) {
902 LoopResult::Ignore("dropbox")
903 } else {
904 LoopResult::LoopSyncStatus(sync_status_res, "dropbox")
905 }
906 },
907 _ = inactivity_timeout_future => {
908 LoopResult::Timeout
909 },
910 };
911 match loop_result {
912 LoopResult::Ignore(_) => {},
913 _ => break,
914 }
915 }
916
917 std::mem::drop(fused_user_selection_future);
918
919 match loop_result {
920 LoopResult::LoopUserSelection(selection) => {
921 s.user_selection = selection;
922 },
923 LoopResult::LoopSyncStatus(sync_status_res, synchronizer_name) => {
924 let (selection, stop_synchronizers) = handle_sync_status(&s.editor, sync_status_res, FILENAME, synchronizer_name).await;
925 s.user_selection = selection;
926 if stop_synchronizers {
927 let _ = s.nc_synchronizer.stop();
928 let _ = s.dbx_synchronizer.stop();
929 }
930 },
931 LoopResult::Timeout => {
932 warn!("Idle time of {} seconds elapsed! Locking...", s.props.idle_timeout_seconds());
933 let message = format!("Idle time of {} seconds elapsed! Locking...", s.props.idle_timeout_seconds());
934 let _ = s.editor.show_message(&message, vec![UserOption::ok()], MessageSeverity::default()).await;
935 s.user_selection = UserSelection::GoTo(Menu::TryPass(false))
936 }
937 LoopResult::Ignore(synchronizer_name) => {
938 let (selection, stop_synchronizers) = handle_sync_status(&s.editor, Ok(SyncStatus::None), FILENAME, synchronizer_name).await;
939 s.user_selection = selection;
940 if stop_synchronizers {
941 let _ = s.nc_synchronizer.stop();
942 let _ = s.dbx_synchronizer.stop();
943 }
944 },
945 }
946
947 Ok((s, stop))
948 }
949}
950
951#[derive(Debug, PartialEq)]
952enum LoopResult {
953 LoopUserSelection(UserSelection),
954 LoopSyncStatus(errors::Result<SyncStatus>, &'static str),
955 Timeout,
956 Ignore(&'static str),
957}
958
959async fn handle_sync_status(editor: &Box<dyn AsyncEditor>, sync_status_res: errors::Result<SyncStatus>, filename: &str, synchronizer_name: &str) -> (UserSelection, bool) {
960 if sync_status_res.is_err() {
961 error!("Error during {synchronizer_name} sync: {:?}", sync_status_res);
962 let _ = editor.show_message(&format!("Synchronization error occured. Please see the logs for more details."), vec![UserOption::ok()], MessageSeverity::Error).await;
963 (UserSelection::GoTo(Menu::Current), true)
964 } else {
965 match sync_status_res.unwrap() {
966 SyncStatus::UploadSuccess(who) => {
967 debug!("The {} server was updated with the local data", who);
968 let _ = editor.show_message(&format!("The {} server was updated with the local data", who), vec![UserOption::ok()], MessageSeverity::Info).await;
969 (UserSelection::GoTo(Menu::Save(true)), false)
970 }
971 SyncStatus::NewAvailable(who, downloaded_filename) => {
972 debug!("Downloaded new data from the {} server.", who);
973 let selection = editor.show_message(&format!("Downloaded new data from the {} server. Do you want to apply them locally now?", who),
974 vec![UserOption::yes(), UserOption::no()],
975 MessageSeverity::Info).await;
976
977 debug!("The user selected {:?} as an answer for applying the downloaded data locally", &selection);
978 if selection == UserSelection::UserOption(UserOption::yes()) {
979 debug!("Replacing the local file with the one downloaded from the server");
980 let _ = file_handler::replace(&downloaded_filename, filename);
981 (UserSelection::GoTo(Menu::TryPass(true)), true)
982 } else {
983 (UserSelection::GoTo(Menu::Current), true)
984 }
985 }
986 SyncStatus::NewToMerge(who, downloaded_filename) => {
987 debug!("Downloaded data from the {} server, but conflicts were identified. The contents will be merged.", who);
988 let selection =
989 editor.show_message(&format!("Downloaded data from the {} server, but conflicts were identified. The contents will be merged \
990 but nothing will be saved. You will need to explicitly save after reviewing the merged data. Do you \
991 want to do the merge now?", who),
992 vec![UserOption::yes(), UserOption::no()],
993 MessageSeverity::Info).await;
994
995 if selection == UserSelection::UserOption(UserOption::yes()) {
996 debug!("The user selected {:?} as an answer for applying the downloaded data locally", &selection);
997 debug!("Merging the local data with the downloaded from the server");
998
999 match editor.show_password_enter().await {
1000 UserSelection::ProvidedPassword(pwd, salt_pos) => {
1001 (UserSelection::ImportFromDefaultLocation(downloaded_filename, pwd, salt_pos), true)
1002 }
1003 other => {
1004 let message = format!("Expected a ProvidedPassword but received '{:?}'. Please, consider opening a bug to the \
1005 developers.",
1006 &other);
1007 error!("{}", message);
1008 let _ =
1009 editor.show_message("Unexpected result when waiting for password. See the logs for more details. Please \
1010 consider opening a bug to the developers.",
1011 vec![UserOption::ok()],
1012 MessageSeverity::Error).await;
1013 (UserSelection::GoTo(Menu::TryPass(false)), true)
1014 }
1015 }
1016 } else {
1017 (UserSelection::GoTo(Menu::Current), true)
1018 }
1019 }
1020 SyncStatus::None => {
1021 let _ = editor.show_message(&format!("{synchronizer_name} synchronization got into unexpected Status. This should never happen theoretically. Please consider opening a bug to the developers."), vec![UserOption::ok()], MessageSeverity::Error).await;
1022 (UserSelection::GoTo(Menu::Current), true)
1023 }
1024 }
1025 }
1026}
1027
1028async fn handle_check_passwords<T>(
1029 safe: &mut Safe,
1030 password_checker: &T,
1031) -> errors::Result<EditorShowMessageWrapper>
1032where
1033 T: PasswordChecker,
1034{
1035 let mut pwned_passwords_found: Option<Vec<String>> = None;
1036 for index in 0..safe.get_entries().len() {
1037 let mut entry = safe.get_entry_decrypted(index);
1038 let pwned_res = password_checker.is_unsafe(&entry.pass).await;
1039 if pwned_res.is_ok() {
1040 let is_pwned = pwned_res.unwrap();
1041 if pwned_passwords_found.is_none() {
1042 pwned_passwords_found = Some(Vec::new());
1043 }
1044 if is_pwned {
1045 pwned_passwords_found
1046 .as_mut()
1047 .unwrap()
1048 .push(entry.name.clone());
1049 }
1050 if is_pwned != entry.meta.leaked_password {
1051 entry.meta.leaked_password = is_pwned;
1052 safe.replace_entry(index, entry)?;
1053 }
1054 } else {
1055 error!("Error while checking passwords: {}", pwned_res.unwrap_err());
1056 pwned_passwords_found = None;
1057 break;
1058 }
1059 }
1060 if pwned_passwords_found.is_none() {
1061 if !safe.get_entries().is_empty() {
1062 Ok(EditorShowMessageWrapper::new(
1063 "Error while checking passwords health. Please see the logs for more details.",
1064 vec![UserOption::ok()],
1065 MessageSeverity::Error,
1066 ))
1067 } else {
1068 Ok(EditorShowMessageWrapper::new(
1069 "No entries to check",
1070 vec![UserOption::ok()],
1071 MessageSeverity::Info,
1072 ))
1073 }
1074 } else {
1075 if !pwned_passwords_found.as_ref().unwrap().is_empty() {
1076 let message = format!(
1077 "The following entries have leaked passwords: {}! Please change them immediately!",
1078 pwned_passwords_found.unwrap().join(",")
1079 );
1080 info!("{}", message);
1081 Ok(EditorShowMessageWrapper::new(
1082 &message,
1083 vec![UserOption::ok()],
1084 MessageSeverity::Warn,
1085 ))
1086 } else {
1087 let message = format!("The passwords of the entries look ok!");
1088 debug!("{}", message);
1089 Ok(EditorShowMessageWrapper::new(
1090 &message,
1091 vec![UserOption::ok()],
1092 MessageSeverity::Info,
1093 ))
1094 }
1095 }
1096}
1097
1098fn bcrypt_cost_from_file() -> u32 {
1099 let props = file_handler::load_properties(PROPS_FILENAME).unwrap_or_default();
1100
1101 if rkl_version() == props.version() || file_handler::is_first_run(FILENAME) {
1103 BCRYPT_COST
1104 } else {
1105 BCRYPT_COST_PRE_0_17_0
1106 }
1107}
1108
1109async fn handle_provided_password_for_init(
1110 provided_password: UserSelection,
1111 filename: &str,
1112 safe: &mut Safe,
1113 configuration: &mut RklConfiguration,
1114 editor: &Box<dyn AsyncEditor>,
1115) -> (UserSelection, datacrypt::BcryptAes) {
1116 let user_selection: UserSelection;
1117 match provided_password {
1118 UserSelection::ProvidedPassword(pwd, salt_pos) => {
1119 let bcrypt_cost = bcrypt_cost_from_file();
1120 info!("Using bcrypt with cost {bcrypt_cost}");
1121 let cr: BcryptAes =
1123 file_handler::create_bcryptor(filename, pwd.to_string(), bcrypt_cost, *salt_pos, false, true)
1124 .unwrap();
1125 let retrieved_entries = match file_handler::load(filename, &cr, true) {
1127 Ok(rkl_content) => {
1129 user_selection = UserSelection::GoTo(Menu::EntriesList(Some("".to_string())));
1130 let new_rkl_conf = RklConfiguration::from((
1132 rkl_content.nextcloud_conf,
1133 rkl_content.dropbox_conf,
1134 rkl_content.system_conf,
1135 rkl_content.general_conf,
1136 ));
1137 *configuration = new_rkl_conf;
1138 rkl_content.entries
1139 }
1140 Err(error) => {
1142 match error {
1143 errors::RustKeylockError::ParseError(desc) => {
1145 warn!("{}", desc);
1146 user_selection = UserSelection::GoTo(Menu::TryFileRecovery);
1147 Vec::new()
1148 }
1149 _ => {
1151 error!("{}", error);
1152 let s =
1153 editor.show_message("Wrong password or number! Please make sure that both the password and number that you \
1154 provide are correct.",
1155 vec![UserOption::ok()],
1156 MessageSeverity::Error)
1157 .await;
1158 match s {
1159 _ => {
1160 user_selection = UserSelection::GoTo(Menu::TryPass(false));
1161 Vec::new()
1162 }
1163 }
1164 }
1165 }
1166 }
1167 };
1168
1169 safe.clear();
1170 safe.add_all(retrieved_entries);
1171 debug!(
1172 "Retrieved entries. Returning {:?} with {} entries ",
1173 &user_selection,
1174 safe.entries.len()
1175 );
1176 let cr = if bcrypt_cost != BCRYPT_COST {
1178 file_handler::create_bcryptor(filename, pwd.to_string(), BCRYPT_COST, *salt_pos, false, true)
1179 .unwrap()
1180 } else {
1181 cr
1182 };
1183 (user_selection, cr)
1184 }
1185 UserSelection::GoTo(Menu::Exit) => {
1186 debug!("UserSelection::GoTo(Menu::Exit) was called before providing credentials");
1187 let cr = file_handler::create_bcryptor(filename, "dummy".to_string(), 1, 33, false, true)
1188 .unwrap();
1189 let exit_selection = UserSelection::GoTo(Menu::ForceExit);
1190 (exit_selection, cr)
1191 }
1192 other => {
1193 panic!("Wrong initialization sequence... The editor.show_password_enter must always return a UserSelection::ProvidedPassword. \
1194 Please, consider opening a bug to the developers.: {:?}", other)
1195 }
1196 }
1197}
1198
1199pub trait Editor {
1203 fn show_password_enter(&self) -> UserSelection;
1205 fn show_change_password(&self) -> UserSelection;
1207 fn show_menu(&self, menu: &Menu) -> UserSelection;
1209 fn show_entries(&self, entries: Vec<Entry>, filter: String) -> UserSelection;
1211 fn show_entry(
1213 &self,
1214 entry: Entry,
1215 index: usize,
1216 presentation_type: EntryPresentationType,
1217 ) -> UserSelection;
1218 fn exit(&self, contents_changed: bool) -> UserSelection;
1220 fn show_configuration(
1222 &self,
1223 nextcloud: NextcloudConfiguration,
1224 dropbox: DropboxConfiguration,
1225 general: GeneralConfiguration,
1226 ) -> UserSelection;
1227 fn show_message(
1230 &self,
1231 message: &str,
1232 options: Vec<UserOption>,
1233 severity: MessageSeverity,
1234 ) -> UserSelection;
1235
1236 fn sort_entries(&self, entries: &mut [Entry]) {
1238 entries.sort_by(|a, b| a.name.to_uppercase().cmp(&b.name.to_uppercase()));
1239 }
1240}
1241
1242#[async_trait]
1246pub trait AsyncEditor {
1247 async fn show_password_enter(&self) -> UserSelection;
1249 async fn show_change_password(&self) -> UserSelection;
1251 async fn show_menu(&self, menu: Menu) -> UserSelection;
1253 async fn show_entries(&self, entries: Vec<Entry>, filter: String) -> UserSelection;
1255 async fn show_entry(
1257 &self,
1258 entry: Entry,
1259 index: usize,
1260 presentation_type: EntryPresentationType,
1261 ) -> UserSelection;
1262 async fn exit(&self, contents_changed: bool) -> UserSelection;
1264 async fn show_configuration(
1266 &self,
1267 nextcloud: NextcloudConfiguration,
1268 dropbox: DropboxConfiguration,
1269 general: GeneralConfiguration,
1270 ) -> UserSelection;
1271 async fn show_message(
1274 &self,
1275 message: &str,
1276 options: Vec<UserOption>,
1277 severity: MessageSeverity,
1278 ) -> UserSelection;
1279
1280 fn sort_entries(&self, entries: &mut [Entry]) {
1282 entries.sort_by(|a, b| a.name.to_uppercase().cmp(&b.name.to_uppercase()));
1283 }
1284
1285 fn start_rest_server(&self) -> bool;
1287}
1288
1289#[cfg(test)]
1290mod unit_tests {
1291 use async_trait::async_trait;
1292
1293 use crate::api::safe::Safe;
1294 use crate::api::EntryMeta;
1295
1296 use super::api::Entry;
1297 use super::*;
1298
1299 struct AlwaysOkTruePasswordChecker {}
1300
1301 #[async_trait]
1302 impl PasswordChecker for AlwaysOkTruePasswordChecker {
1303 async fn is_unsafe(&self, _: &str) -> errors::Result<bool> {
1304 Ok(true)
1305 }
1306 }
1307
1308 struct AlwaysOkFalsePasswordChecker {}
1309
1310 #[async_trait]
1311 impl PasswordChecker for AlwaysOkFalsePasswordChecker {
1312 async fn is_unsafe(&self, _: &str) -> errors::Result<bool> {
1313 Ok(false)
1314 }
1315 }
1316
1317 struct AlwaysErrorPasswordChecker {}
1318
1319 #[async_trait]
1320 impl PasswordChecker for AlwaysErrorPasswordChecker {
1321 async fn is_unsafe(&self, _: &str) -> errors::Result<bool> {
1322 Err(errors::RustKeylockError::GeneralError("".to_string()))
1323 }
1324 }
1325
1326 #[tokio::test]
1327 async fn test_handle_check_passwords() {
1328 let mut safe = Safe::default();
1329
1330 let smw = handle_check_passwords(&mut safe, &AlwaysOkTruePasswordChecker {})
1332 .await
1333 .unwrap();
1334 assert!(&smw.message == "No entries to check");
1335
1336 safe.add_entry(Entry::new(
1338 "name".to_string(),
1339 "url".to_string(),
1340 "user".to_string(),
1341 "pass".to_string(),
1342 "desc".to_string(),
1343 EntryMeta::default(),
1344 ));
1345 let smw = handle_check_passwords(&mut safe, &AlwaysOkFalsePasswordChecker {})
1346 .await
1347 .unwrap();
1348 assert!(&smw.message == "The passwords of the entries look ok!");
1349
1350 let smw = handle_check_passwords(&mut safe, &AlwaysOkTruePasswordChecker {})
1352 .await
1353 .unwrap();
1354 assert!(&smw.message == "The following entries have leaked passwords: name! Please change them immediately!");
1355
1356 let smw = handle_check_passwords(&mut safe, &AlwaysErrorPasswordChecker {})
1358 .await
1359 .unwrap();
1360 assert!(
1361 &smw.message
1362 == "Error while checking passwords health. Please see the logs for more details."
1363 );
1364 }
1365}