rust_keylock/
lib.rs

1// Copyright 2017 astonbitecode
2// This file is part of rust-keylock password manager.
3//
4// rust-keylock is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// rust-keylock is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with rust-keylock.  If not, see <http://www.gnu.org/licenses/>.
16
17//! # The _rust-keylock_ library
18//!
19//! Executes the logic of the _rust-keylock_.
20//!
21//! This library is the executor of the _rust-keylock_ logic. `Editor` references are used to interact with the _rust-keylock_ users.
22
23extern 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
61// Read the code version
62include!(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
86/// Takes a reference of `Editor` implementation as argument and executes the _rust-keylock_ logic.
87/// The `Editor` is responsible for the interaction with the user. Currently there are `Editor` implementations for __shell__ and for __Android__.
88pub 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    // Holds the UserSelections
147    user_selection: UserSelection,
148    // Keeps the sensitive data
149    safe: Safe,
150    configuration: RklConfiguration,
151    dbx_synchronizer: dropbox::Synchronizer,
152    nc_synchronizer: nextcloud::Synchronizer,
153    // Signals changes that are not saved
154    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        // Holds the UserSelections
164        let user_selection;
165
166        // Keeps the sensitive data
167        let mut safe = Safe::new();
168        // Keeps the configuration data
169        let mut configuration = RklConfiguration::from((
170            nextcloud::NextcloudConfiguration::default(),
171            dropbox::DropboxConfiguration::default(),
172            SystemConfiguration::default(),
173            GeneralConfiguration::default(),
174        ));
175        // Signals changes that are not saved
176        let contents_changed = false;
177        // Create a Cryptor
178        let cryptor: BcryptAes;
179        loop {
180            // First time run?
181            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            // Take the provided password and do the initialization
188            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 the password was correct
196            if us != UserSelection::GoTo(Menu::TryPass(false)) {
197                // Save the version
198                props.set_version(rkl_version());
199                let _ = file_handler::save_props(&props, PROPS_FILENAME);
200                // Set the UserSelection
201                user_selection = us;
202                cryptor = cr;
203                break;
204            }
205        };
206
207        // Initialize the synchronizers
208        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    // This is the main function that handles all the user selections. Its complexity is expected to be big.
244    // This may change in the future during a refactoring but is accepted for now.
245    #[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        // Handle
252        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                    // Reset the filter
351                    s.safe.set_filter("".to_string());
352                    // Initialize the synchronizers
353                    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                            // Clean the flag for unsaved data
395                            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                                // Mark contents changed
683                                s.contents_changed = true;
684                                // Do the merge
685                                s.safe.merge(rkl_content.entries);
686                                // Replace the configuration
687                                s.configuration.system = rkl_content.system_conf;
688                                // Make the last_sync_version equal to the local one.
689                                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                        // Stop the synchronizer if running
746                        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                        // Stop the synchronizer if running
756                        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                // Do not change Menu
781                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        // Prepare all the possible futures from which we expect possible user_selection
879        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            // Get the first future that completes
889            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(_) => {/* Ignore this. Continue the loop */},
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    // The bcrypt cost changed in 0.17.0 from 7 to 12
1102    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            // Create cryptor for decryption
1122            let cr: BcryptAes =
1123                file_handler::create_bcryptor(filename, pwd.to_string(), bcrypt_cost, *salt_pos, false, true)
1124                    .unwrap();
1125            // Try to decrypt and load the Entries
1126            let retrieved_entries = match file_handler::load(filename, &cr, true) {
1127                // Success, go to the List of entries
1128                Ok(rkl_content) => {
1129                    user_selection = UserSelection::GoTo(Menu::EntriesList(Some("".to_string())));
1130                    // Set the retrieved configuration
1131                    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                // Failure cases
1141                Err(error) => {
1142                    match error {
1143                        // If Parse error, try recovery
1144                        errors::RustKeylockError::ParseError(desc) => {
1145                            warn!("{}", desc);
1146                            user_selection = UserSelection::GoTo(Menu::TryFileRecovery);
1147                            Vec::new()
1148                        }
1149                        // In all the other cases, notify the User and retry
1150                        _ => {
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            // Return cryptor with the correct current bcrypt cost
1177            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
1199/// Trait to be implemented by various different `Editor`s (Shell, Web, Android, other...).
1200///
1201/// It drives the interaction with the Users
1202pub trait Editor {
1203    /// Shows the interface for entering a Password and a Number.
1204    fn show_password_enter(&self) -> UserSelection;
1205    /// Shows the interface for changing a Password and/or a Number.
1206    fn show_change_password(&self) -> UserSelection;
1207    /// Shows the specified `Menu` to the User.
1208    fn show_menu(&self, menu: &Menu) -> UserSelection;
1209    /// Shows the provided entries to the User. The provided entries are already filtered with the filter argument.
1210    fn show_entries(&self, entries: Vec<Entry>, filter: String) -> UserSelection;
1211    /// Shows the provided entry details to the User following a PresentationType.
1212    fn show_entry(
1213        &self,
1214        entry: Entry,
1215        index: usize,
1216        presentation_type: EntryPresentationType,
1217    ) -> UserSelection;
1218    /// Shows the Exit `Menu` to the User.
1219    fn exit(&self, contents_changed: bool) -> UserSelection;
1220    /// Shows the configuration screen.
1221    fn show_configuration(
1222        &self,
1223        nextcloud: NextcloudConfiguration,
1224        dropbox: DropboxConfiguration,
1225        general: GeneralConfiguration,
1226    ) -> UserSelection;
1227    /// Shows a message to the User.
1228    /// Along with the message, the user should select one of the offered `UserOption`s.
1229    fn show_message(
1230        &self,
1231        message: &str,
1232        options: Vec<UserOption>,
1233        severity: MessageSeverity,
1234    ) -> UserSelection;
1235
1236    /// Sorts the supplied entries.
1237    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/// Trait to be implemented by various different `Editor`s (Shell, Web, Android, other...).
1243///
1244/// It drives the interaction with the Users
1245#[async_trait]
1246pub trait AsyncEditor {
1247    /// Shows the interface for entering a Password and a Number.
1248    async fn show_password_enter(&self) -> UserSelection;
1249    /// Shows the interface for changing a Password and/or a Number.
1250    async fn show_change_password(&self) -> UserSelection;
1251    /// Shows the specified `Menu` to the User.
1252    async fn show_menu(&self, menu: Menu) -> UserSelection;
1253    /// Shows the provided entries to the User. The provided entries are already filtered with the filter argument.
1254    async fn show_entries(&self, entries: Vec<Entry>, filter: String) -> UserSelection;
1255    /// Shows the provided entry details to the User following a presentation type.
1256    async fn show_entry(
1257        &self,
1258        entry: Entry,
1259        index: usize,
1260        presentation_type: EntryPresentationType,
1261    ) -> UserSelection;
1262    /// Shows the Exit `Menu` to the User.
1263    async fn exit(&self, contents_changed: bool) -> UserSelection;
1264    /// Shows the configuration screen.
1265    async fn show_configuration(
1266        &self,
1267        nextcloud: NextcloudConfiguration,
1268        dropbox: DropboxConfiguration,
1269        general: GeneralConfiguration,
1270    ) -> UserSelection;
1271    /// Shows a message to the User.
1272    /// Along with the message, the user should select one of the offered `UserOption`s.
1273    async fn show_message(
1274        &self,
1275        message: &str,
1276        options: Vec<UserOption>,
1277        severity: MessageSeverity,
1278    ) -> UserSelection;
1279
1280    /// Sorts the supplied entries.
1281    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    /// Denotes if the rest_server should be start or nor
1286    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        // No entries to check
1331        let smw = handle_check_passwords(&mut safe, &AlwaysOkTruePasswordChecker {})
1332            .await
1333            .unwrap();
1334        assert!(&smw.message == "No entries to check");
1335
1336        // Entries Ok and healthy
1337        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        // Entries Ok but not healthy
1351        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        // Entries Error
1357        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}