Add additive KDBX importer
This commit is contained in:
1 parent
f84f7247d2
commit
bf75465642
23 files changed
+1959
-82
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user