From 3e6a48d7e7a94a790680c482d287bd724e41ca57 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Mon, 8 Sep 2025 12:36:56 +0200 Subject: [PATCH] feat: add import of PKCS #12 files --- README.md | 3 +- ui/src/main.rs | 140 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 110 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 0073652..d78d5e0 100644 --- a/README.md +++ b/README.md @@ -65,4 +65,5 @@ interface is available in (sub-)package `ui`. ![](ui-image.jpeg) -The GUI also provides export to a password protected PKCS #12 file, including certificates and private keys. \ No newline at end of file +The GUI also provides import from and export to a password protected PKCS #12 file, +including certificates and private keys. \ No newline at end of file diff --git a/ui/src/main.rs b/ui/src/main.rs index c874991..ea95c3c 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -19,17 +19,14 @@ #![windows_subsystem = "windows"] -use cert_tools::{save_p12_file, Chain, PrivateKey}; +use cert_tools::{read_p12_file, save_p12_file, Chain, PrivateKey}; use iced::border::Radius; use iced::widget::text_editor::{default, Content, Status}; use iced::widget::{ self, button, column, container, horizontal_rule, horizontal_space, row, text, text_editor, text_input, Container, Scrollable, }; -use iced::{ - alignment, application, clipboard, color, window, Background, Border, Color, Element, Font, - Length, Pixels, Settings, Size, Task, -}; +use iced::{alignment, application, clipboard, color, window, Background, Border, Color, Element, Font, Length, Padding, Pixels, Settings, Size, Task}; use std::fs; use std::path::PathBuf; use std::time::SystemTime; @@ -66,7 +63,8 @@ impl File { enum UiMode { CertList, Output, - Passphrase, + ImportPassphrase, + ExportPassphrase, } struct Ui { @@ -119,7 +117,7 @@ impl Ui { } } - self.mode = UiMode::CertList; + //self.mode = UiMode::CertList; match message { Message::PickCertFile => Task::perform(pick_file(), Message::SetCertFile), Message::PickCaFile => Task::perform(pick_file(), Message::SetCaFile), @@ -149,6 +147,10 @@ impl Ui { Message::SetCertFile(file) => { match file { Ok(file) => { + if file.to_str().unwrap_or_default().to_lowercase().ends_with(".p12") { + self.cert_file = File::Certificates(file, Box::new(Chain::from(vec![]))); + return Task::done(Message::AskForImportPassword) + } self.cert_file = match Chain::read(&file) { Ok(chain) => File::Certificates(file, Box::new(chain)), Err(_) => File::Invalid(file), @@ -192,12 +194,40 @@ impl Ui { self.key_indicator_state = self.key_indicator_state(); Task::done(Message::Print) } + Message::SetPkcs12File(file) => { + match file { + Ok(file) => { + let (cert_file, key_file) = match read_p12_file(&file, &self.password_1) { + Ok((chain, key)) => ( + File::Certificates(file.clone(), Box::new(chain)), + File::PrivateKey(file, Box::new(key)) + ), + Err(_) => ( + File::Invalid(file.clone()), + File::Invalid(file) + ) + }; + self.cert_file = cert_file; + self.key_file = key_file; + self.chain = self.load_chain().ok(); + self.fixed_chain = fixed_chain(&self.chain); + self.output = Content::default(); + self.mode = UiMode::CertList; + self.password_1 = String::new(); + self.password_2 = String::new(); + } + _ => self.cert_file = File::None + } + self.chain_indicator_state = self.chain_indicator_state(); + self.key_indicator_state = self.key_indicator_state(); + Task::done(Message::Print) + } Message::Print => { + self.mode = UiMode::CertList; match self.print_output() { Ok(output) => { self.output = Content::with_text(output.as_str()); self.status = String::new(); - self.mode = UiMode::CertList; } Err(err) => { self.output = Content::default(); @@ -207,6 +237,7 @@ impl Ui { Task::none() } Message::PrintPem => { + self.mode = UiMode::CertList; match self.pem_output() { Ok(output) => { self.output = Content::with_text(output.as_str()); @@ -222,6 +253,7 @@ impl Ui { } Message::CopyValue(value) => clipboard::write::(value), Message::Cleanup => { + self.mode = UiMode::CertList; if let Some(chain) = self.fixed_chain.take() { self.chain = Some(chain); self.mode = UiMode::CertList; @@ -245,14 +277,19 @@ impl Ui { } Task::none() } - Message::AskForPassword => { - self.mode = UiMode::Passphrase; + Message::AskForImportPassword => { + self.mode = UiMode::ImportPassphrase; + Task::none() + } + Message::AskForExportPassword => { + self.mode = UiMode::ExportPassphrase; Task::none() } Message::PickExportP12File => { Task::perform(export_p12_file(), Message::ExportToP12File) } Message::ExportToP12File(file) => { + self.mode = UiMode::CertList; let private_key = match &self.key_file { File::PrivateKey(_, key) => Some(key.as_ref().clone()), _ => None, @@ -279,12 +316,12 @@ impl Ui { Task::none() } Message::SetPw1(pw) => { - self.mode = UiMode::Passphrase; + //self.mode = UiMode::ExportPassphrase; self.password_1 = pw.clone(); Task::none() } Message::SetPw2(pw) => { - self.mode = UiMode::Passphrase; + //self.mode = UiMode::ExportPassphrase; self.password_2 = pw.clone(); Task::none() } @@ -405,7 +442,7 @@ impl Ui { || self.chain_indicator_state == IndicatorState::Cleaned) && self.key_indicator_state == IndicatorState::Success { button("Export PKCS #12") - .on_press(Message::AskForPassword) + .on_press(Message::AskForExportPassword) .style(button::primary) } else { button("Export PKCS #12").style(button::primary) @@ -747,28 +784,64 @@ impl Ui { .center_y(80) }; - let ask_for_password = { + let ask_for_import_password = { + let file = match &self.cert_file { + File::Certificates(file, _) => Ok(file.clone()), + _ => Err(Error::Undefined), + }; + row![ + column![].width(Length::Fill), + container( + column![ + text("Bitte Passwort für den Import eingeben"), + text_input("", &self.password_1) + .secure(true) + .on_input(Message::SetPw1) + .on_submit(Message::SetPkcs12File(file.clone())), + row![ + button("OK").on_press(Message::SetPkcs12File(file)), + button("Cancel").on_press(Message::Abort) + ].spacing(4), + ] + .spacing(4) + .height(Length::Fill) + .width(320), + ) + .center_x(320), + column![].width(Length::Fill), + ] + .padding(Padding::from(64)) + .height(Length::Fill) + .width(Length::Fill) + }; + + let ask_for_export_password = { let ok_button = if !self.password_1.is_empty() && self.password_1 == self.password_2 { button("OK").on_press(Message::PickExportP12File) } else { button("OK") }; - row![container( - column![ - text("Bitte Passwort für den Export eingeben"), - text_input("", &self.password_1) - .secure(true) - .on_input(Message::SetPw1), - text_input("", &self.password_2) - .secure(true) - .on_input(Message::SetPw2), - row![ok_button, button("Cancel").on_press(Message::Abort)].spacing(4), - ] - .spacing(4) - .height(Length::Fill) - .width(320), - ) - .center_x(320)] + row![ + column![].width(Length::Fill), + container( + column![ + text("Bitte Passwort für den Export eingeben"), + text_input("", &self.password_1) + .secure(true) + .on_input(Message::SetPw1), + text_input("", &self.password_2) + .secure(true) + .on_input(Message::SetPw2), + row![ok_button, button("Cancel").on_press(Message::Abort)].spacing(4), + ] + .spacing(4) + .height(Length::Fill) + .width(320), + ) + .center_x(320), + column![].width(Length::Fill), + ] + .padding(Padding::from(64)) .height(Length::Fill) .width(Length::Fill) }; @@ -786,7 +859,8 @@ impl Ui { match self.mode { UiMode::CertList => column![certs, chain_info], UiMode::Output => column![output], - UiMode::Passphrase => column![ask_for_password], + UiMode::ImportPassphrase => column![ask_for_import_password], + UiMode::ExportPassphrase => column![ask_for_export_password], }, horizontal_rule(1), row![ @@ -997,6 +1071,7 @@ enum Message { SetCertFile(Result), SetCaFile(Result), SetKeyFile(Result), + SetPkcs12File(Result), Print, PrintPem, CopyValue(String), @@ -1005,7 +1080,8 @@ enum Message { ExportToFile(Result), PickExportP12File, ExportToP12File(Result), - AskForPassword, + AskForImportPassword, + AskForExportPassword, SetPw1(String), SetPw2(String), Abort,