Add additive KDBX importer

This commit is contained in:
hugo committed 2026-08-11 09:04:48 +02:00
1 parent f84f7247d2
commit bf75465642
23 files changed
+1959 -82

No files matched your search

+127
View File
@@ -10,7 +10,9 @@ use ironstorage::{
RemoveRequest,
},
crypto::KeyInfo,
kdbx::{KdbxImportMode, KdbxImportRequest},
otp::OtpInput,
repository::SecretBytes,
write::{InsertContent, OverwriteDecision},
};
use zeroize::Zeroize;
@@ -120,6 +122,16 @@ pub struct GrepForm {
focus: usize,
}
#[derive(Debug)]
pub struct KdbxForm {
source: String,
key_file: String,
password: SecretText,
quick_add: bool,
confirmed: bool,
focus: usize,
}
#[derive(Debug)]
pub struct RemoveForm {
target: String,
@@ -161,6 +173,7 @@ pub enum WorkflowForm {
Insert(InsertForm),
Generate(GenerateForm),
Grep(GrepForm),
Kdbx(KdbxForm),
Remove(RemoveForm),
Move(TransferForm),
Copy(TransferForm),
@@ -180,6 +193,10 @@ pub enum WorkflowSubmission {
overwrite: OverwriteDecision,
},
Grep(GrepRequest),
Kdbx {
request: KdbxImportRequest,
password: SecretBytes,
},
Remove(RemoveRequest),
Move {
request: MoveRequest,
@@ -321,6 +338,21 @@ impl WorkflowForm {
})
}
pub fn kdbx(request: Option<KdbxImportRequest>) -> Self {
let request = request
.unwrap_or_else(|| KdbxImportRequest::new("", None, KdbxImportMode::AddAndUpdate));
Self::Kdbx(KdbxForm {
source: request.source().to_string_lossy().into_owned(),
key_file: request
.key_file()
.map_or_else(String::new, |path| path.to_string_lossy().into_owned()),
password: SecretText::default(),
quick_add: request.mode() == KdbxImportMode::QuickAdd,
confirmed: false,
focus: 0,
})
}
pub fn remove(request: Option<RemoveRequest>, selected: Option<(&str, bool)>) -> Self {
let request = request.unwrap_or_else(|| RemoveRequest {
entry: selected.map_or_else(String::new, |(path, _)| path.to_owned()),
@@ -368,6 +400,7 @@ impl WorkflowForm {
Self::Insert(_) => "Insert entry",
Self::Generate(_) => "Generate password",
Self::Grep(_) => "Search decrypted entries",
Self::Kdbx(_) => "Import KeePass KDBX",
Self::Remove(_) => "Remove entry or folder",
Self::Move(_) => "Move or rename",
Self::Copy(_) => "Copy entry or folder",
@@ -453,6 +486,33 @@ impl WorkflowForm {
row(form.focus == 3, "Line numbers", yes_no(form.line_number)),
row(form.focus == 4, "Fixed string", yes_no(form.fixed_strings)),
],
Self::Kdbx(form) => vec![
row(form.focus == 0, "KDBX file", &form.source),
row(
form.focus == 1,
"Key file",
if form.key_file.is_empty() {
"(none)"
} else {
&form.key_file
},
),
row(
form.focus == 2,
"Database password",
if form.password.is_empty() {
"(empty)"
} else {
"••••••••"
},
),
row(
form.focus == 3,
"Only add new entries",
yes_no(form.quick_add),
),
row(form.focus == 4, "Confirm import", yes_no(form.confirmed)),
],
Self::Remove(form) => vec![
row(form.focus == 0, "Target", &form.target),
row(form.focus == 1, "Recursive folder", yes_no(form.recursive)),
@@ -633,6 +693,26 @@ impl WorkflowForm {
fixed_strings: form.fixed_strings,
}))
}
Self::Kdbx(form) => {
if form.source.trim().is_empty() {
return Err("KDBX file path is required".to_owned());
}
if !form.confirmed {
return Err("Explicitly confirm the KDBX import".to_owned());
}
Ok(WorkflowSubmission::Kdbx {
request: KdbxImportRequest::new(
form.source.trim(),
(!form.key_file.trim().is_empty()).then(|| form.key_file.trim().into()),
if form.quick_add {
KdbxImportMode::QuickAdd
} else {
KdbxImportMode::AddAndUpdate
},
),
password: SecretBytes::new(form.password.bytes()),
})
}
Self::Remove(form) => {
if form.target.trim().is_empty() {
return Err("Removal target is required".to_owned());
@@ -718,6 +798,7 @@ impl WorkflowForm {
Self::Insert(_) => 5,
Self::Generate(_) => 6,
Self::Grep(_) => 5,
Self::Kdbx(_) => 5,
Self::Remove(_) | Self::Move(_) | Self::Copy(_) => 4,
Self::Otp(_) => 4,
}
@@ -730,6 +811,7 @@ impl WorkflowForm {
Self::Insert(form) => &mut form.focus,
Self::Generate(form) => &mut form.focus,
Self::Grep(form) => &mut form.focus,
Self::Kdbx(form) => &mut form.focus,
Self::Remove(form) => &mut form.focus,
Self::Move(form) | Self::Copy(form) => &mut form.focus,
Self::Otp(form) => &mut form.focus,
@@ -770,6 +852,8 @@ impl WorkflowForm {
Self::Grep(form) if form.focus == 2 => form.invert_match ^= true,
Self::Grep(form) if form.focus == 3 => form.line_number ^= true,
Self::Grep(form) if form.focus == 4 => form.fixed_strings ^= true,
Self::Kdbx(form) if form.focus == 3 => form.quick_add ^= true,
Self::Kdbx(form) if form.focus == 4 => form.confirmed ^= true,
Self::Remove(form) if form.focus == 1 => form.recursive ^= true,
Self::Remove(form) if form.focus == 2 => form.force ^= true,
Self::Remove(form) if form.focus == 3 => form.confirmed ^= true,
@@ -813,6 +897,13 @@ impl WorkflowForm {
Self::Grep(form) if form.focus == 0 => {
form.pattern.pop();
}
Self::Kdbx(form) if form.focus == 0 => {
form.source.pop();
}
Self::Kdbx(form) if form.focus == 1 => {
form.key_file.pop();
}
Self::Kdbx(form) if form.focus == 2 => form.password.pop(),
Self::Remove(form) if form.focus == 0 => {
form.target.pop();
}
@@ -851,6 +942,11 @@ impl WorkflowForm {
form.length.push(character)
}
Self::Grep(form) if form.focus == 0 => form.pattern.push(character),
Self::Kdbx(form) if form.focus == 0 => form.source.push(character),
Self::Kdbx(form) if form.focus == 1 => form.key_file.push(character),
Self::Kdbx(form) if form.focus == 2 && character != '\n' => {
form.password.push(character)
}
Self::Remove(form) if form.focus == 0 => form.target.push(character),
Self::Move(form) | Self::Copy(form) if form.focus == 0 => form.source.push(character),
Self::Move(form) | Self::Copy(form) if form.focus == 1 => {
@@ -919,6 +1015,37 @@ fn generated_presentation_label(presentation: GeneratedPresentation) -> &'static
mod tests {
use super::*;
#[test]
fn kdbx_form_keeps_password_secret_and_submits_both_import_modes() {
let mut form = WorkflowForm::kdbx(Some(KdbxImportRequest::new(
"fixture.kdbx",
Some("fixture.key".into()),
KdbxImportMode::AddAndUpdate,
)));
form.handle_key(KeyCode::Tab, KeyModifiers::NONE);
form.handle_key(KeyCode::Tab, KeyModifiers::NONE);
for character in "database password".chars() {
form.handle_key(KeyCode::Char(character), KeyModifiers::NONE);
}
form.handle_key(KeyCode::Tab, KeyModifiers::NONE);
form.handle_key(KeyCode::Char(' '), KeyModifiers::NONE);
form.handle_key(KeyCode::Tab, KeyModifiers::NONE);
form.handle_key(KeyCode::Char(' '), KeyModifiers::NONE);
let WorkflowSubmission::Kdbx { request, password } =
form.submission().expect("confirmed import")
else {
panic!("expected KDBX submission");
};
assert_eq!(request.source(), std::path::Path::new("fixture.kdbx"));
assert_eq!(
request.key_file(),
Some(std::path::Path::new("fixture.key"))
);
assert_eq!(request.mode(), KdbxImportMode::QuickAdd);
assert_eq!(password.expose(), b"database password");
}
fn type_text(form: &mut WorkflowForm, text: &str) {
for character in text.chars() {
form.handle_key(KeyCode::Char(character), KeyModifiers::NONE);