feat: support keyfiles

I don't personally use them, so this is untested,
but I imagine it works.

in addition, I removed an unecessary bool "db_loaded",
changed the style of the picker buttons based on whether the files exist,
and improved ordering of the State struct
This commit is contained in:
electria 2026-08-28 21:45:38 -07:00
commit 6e7d50a79d
Signed by: electria
SSH key fingerprint: SHA256:8LlB3ucPbBHqozqkhsNbaV5oG3SlzzqUj8FZDL6IPQs

View file

@ -17,6 +17,10 @@ enum Message {
PickDb,
PickedDb(Option<FileHandle>),
ChangeKeyfilePath(String),
PickKeyfile,
PickedKeyfile(Option<FileHandle>),
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<keepass::Database>,
db_loaded: bool,
db_view: Vec<keepass::db::Entry>,
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())?;