diff --git a/src/app/mod.rs b/src/app/mod.rs index 75cc996..eae00ff 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -17,6 +17,10 @@ enum Message { PickDb, PickedDb(Option), + ChangeKeyfilePath(String), + PickKeyfile, + PickedKeyfile(Option), + ChangePassword(String), ToggleSecurity, OpenDb, @@ -48,18 +52,19 @@ struct State { db_path: String, db_path_exists: bool, + keyfile_path: String, + keyfile_path_exists: bool, + + is_secure: bool, + password: String, + db: Option, - db_loaded: bool, db_view: Vec, selected_entry: usize, selected_field: db::BasicField, query: String, - password: String, - - is_secure: bool, - status: Cow<'static, str>, error: Cow<'static, str>, } @@ -103,6 +108,22 @@ impl State { } Message::PickedDb(None) => eprintln!("no path received from picker"), + Message::ChangeKeyfilePath(keyfile_path) => { + self.keyfile_path_exists = fs::exists(&keyfile_path) + .inspect_err(|e| eprintln!("failed to check '{keyfile_path}': {e}")) + .is_ok_and(|exists| exists); + self.keyfile_path = keyfile_path + } + Message::PickKeyfile => { + return Task::perform(AsyncFileDialog::new().pick_file(), Message::PickedKeyfile); + } + Message::PickedKeyfile(Some(file)) => { + return Task::done(Message::ChangeKeyfilePath( + file.path().to_string_lossy().to_string(), + )); + } + Message::PickedKeyfile(None) => eprintln!("no path received from picker"), + Message::ChangePassword(password) => self.password = password, Message::ToggleSecurity => self.is_secure = !self.is_secure, Message::OpenDb => return self.open_db(), @@ -113,7 +134,6 @@ impl State { self.error = "".into(); self.db = Some(db); self.populate_view(); - self.db_loaded = true; } Err(e) => self.error = e.into(), } @@ -172,7 +192,7 @@ impl State { Task::none() } fn view(&self) -> Element<'_> { - let main = if self.db_loaded { + let main = if self.db.is_some() { widget::column([ widget::text_input("search query", &self.query) .on_input(Message::ChangeQuery) @@ -263,7 +283,30 @@ impl State { widget::text_input("database path", &self.db_path) .on_input(Message::ChangeDbPath) .into(), - widget::button("pick file").on_press(Message::PickDb).into(), + widget::button("pick db") + .style(if self.db_path_exists { + widget::button::primary + } else { + widget::button::danger + }) + .on_press(Message::PickDb) + .into(), + ]) + .into(), + widget::row([ + widget::text_input("keyfile path", &self.keyfile_path) + .on_input(Message::ChangeKeyfilePath) + .into(), + widget::button("pick keyfile") + .style( + if self.keyfile_path.is_empty() || self.keyfile_path_exists { + widget::button::primary + } else { + widget::button::danger + }, + ) + .on_press(Message::PickKeyfile) + .into(), ]) .into(), widget::row([ @@ -279,7 +322,9 @@ impl State { .spacing(10) .into(), widget::container(widget::button("open database").on_press_maybe( - if self.db_path_exists { + if self.db_path_exists + && (self.keyfile_path.is_empty() || self.keyfile_path_exists) + { Some(Message::OpenDb) } else { None @@ -376,12 +421,32 @@ impl State { self.status = "loading...".into(); - let (db_path, password) = (self.db_path.clone(), self.password.clone()); + let (db_path, keyfile_path, password) = ( + self.db_path.clone(), + self.keyfile_path.clone(), + self.password.clone(), + ); Task::perform( async move { + let key = { + let mut key = keepass::DatabaseKey::new(); + if !password.is_empty() { + key = key.with_password(&password); + } + if !keyfile_path.is_empty() { + key = key + .with_keyfile( + &mut fs::File::open(&keyfile_path).map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string())?; + } + + key + }; + let db = keepass::Database::open( &mut fs::File::open(&db_path).map_err(|e| e.to_string())?, - keepass::DatabaseKey::new().with_password(&password), + key, ) .map_err(|e| e.to_string())?;