diff --git a/apps/plumeimpactor/src/certificate_reset.rs b/apps/plumeimpactor/src/certificate_reset.rs new file mode 100644 index 0000000..1c56270 --- /dev/null +++ b/apps/plumeimpactor/src/certificate_reset.rs @@ -0,0 +1,63 @@ +use std::sync::{Mutex, OnceLock, mpsc}; + +pub(crate) const WARNING: &str = "Impactor needs to reset your certificate. This breaks existing SideStore and AltStore installs."; + +#[derive(Debug, Clone)] +pub struct ConfirmationRequest { + pub message: String, + responder: mpsc::Sender, +} + +impl ConfirmationRequest { + pub fn respond(&self, accepted: bool) { + let _ = self.responder.send(accepted); + } +} + +static REQUEST_TX: OnceLock> = OnceLock::new(); +static REQUEST_RX: OnceLock>> = OnceLock::new(); + +fn request_channel() -> ( + &'static mpsc::Sender, + &'static Mutex>, +) { + REQUEST_TX.get_or_init(|| { + let (tx, rx) = mpsc::channel(); + let _ = REQUEST_RX.set(Mutex::new(rx)); + tx + }); + + ( + REQUEST_TX + .get() + .expect("request sender should be initialized"), + REQUEST_RX + .get() + .expect("request receiver should be initialized"), + ) +} + +pub fn request_confirmation(message: &str) -> bool { + let (response_tx, response_rx) = mpsc::channel(); + let request = ConfirmationRequest { + message: message.to_string(), + responder: response_tx, + }; + + let (request_tx, _) = request_channel(); + if request_tx.send(request).is_err() { + return false; + } + + response_rx.recv().unwrap_or(false) +} + +pub fn confirm() -> bool { + log::warn!("{WARNING}"); + request_confirmation(WARNING) +} + +pub fn wait_for_request() -> Option { + let (_, request_rx) = request_channel(); + request_rx.lock().ok()?.recv().ok() +} diff --git a/apps/plumeimpactor/src/main.rs b/apps/plumeimpactor/src/main.rs index f2f5a97..5b14e69 100644 --- a/apps/plumeimpactor/src/main.rs +++ b/apps/plumeimpactor/src/main.rs @@ -6,6 +6,7 @@ use crate::refresh::spawn_refresh_daemon; use single_instance::SingleInstance; mod appearance; +mod certificate_reset; mod defaults; mod macos_app; mod refresh; diff --git a/apps/plumeimpactor/src/refresh.rs b/apps/plumeimpactor/src/refresh.rs index 5fd96a0..8ea1b42 100644 --- a/apps/plumeimpactor/src/refresh.rs +++ b/apps/plumeimpactor/src/refresh.rs @@ -195,12 +195,14 @@ impl RefreshDaemon { }; let identity_is_new = { + let mut on_certificate_reset = crate::certificate_reset::confirm; let identity = CertificateIdentity::new_with_session( &session, get_data_path(), None, team_id, false, + Some(&mut on_certificate_reset), ) .await .map_err(|e| format!("Failed to create identity: {}", e))?; @@ -274,12 +276,14 @@ impl RefreshDaemon { }; let team_id_string = team_id.to_string(); + let mut on_certificate_reset = crate::certificate_reset::confirm; let signing_identity = CertificateIdentity::new_with_session( session, get_data_path(), None, &team_id_string, false, + Some(&mut on_certificate_reset), ) .await .map_err(|e| format!("Failed to create signing identity: {}", e))?; diff --git a/apps/plumeimpactor/src/screen/mod.rs b/apps/plumeimpactor/src/screen/mod.rs index 3d66ac4..518b963 100644 --- a/apps/plumeimpactor/src/screen/mod.rs +++ b/apps/plumeimpactor/src/screen/mod.rs @@ -5,8 +5,10 @@ pub(crate) mod settings; mod utilties; mod windows; +use std::collections::VecDeque; + use iced::Length::Fill; -use iced::widget::{button, container, pick_list, row, text}; +use iced::widget::{button, column, container, pick_list, row, stack, text}; use iced::window; use iced::{Element, Subscription, Task}; @@ -72,6 +74,9 @@ pub enum Message { SettingsScreen(settings::Message), InstallerScreen(package::Message), ProgressScreen(progress::Message), + CertificateResetRequested(crate::certificate_reset::ConfirmationRequest), + ConfirmCertificateReset, + CancelCertificateReset, // Installation StartInstallation, @@ -87,6 +92,7 @@ pub struct Impactor { account_store: Option, login_windows: std::collections::HashMap, pending_installation: bool, + certificate_reset_queue: VecDeque, } #[derive(Debug, Clone, PartialEq)] @@ -132,6 +138,7 @@ impl Impactor { account_store: Some(store), login_windows: std::collections::HashMap::new(), pending_installation: false, + certificate_reset_queue: VecDeque::new(), }, open_task, ) @@ -142,6 +149,18 @@ impl Impactor { AccountStore::load_sync(&Some(path)).unwrap_or_default() } + fn respond_to_next_certificate_reset(&mut self, accepted: bool) { + if let Some(request) = self.certificate_reset_queue.pop_front() { + request.respond(accepted); + } + } + + fn cancel_pending_certificate_resets(&mut self) { + while let Some(request) = self.certificate_reset_queue.pop_front() { + request.respond(false); + } + } + pub fn update(&mut self, message: Message) -> Task { match message { Message::ComboBoxSelected(value) => { @@ -312,6 +331,22 @@ impl Impactor { Task::none() } } + Message::CertificateResetRequested(request) => { + self.certificate_reset_queue.push_back(request); + if self.main_window.is_none() { + Task::done(Message::ShowWindow) + } else { + Task::none() + } + } + Message::ConfirmCertificateReset => { + self.respond_to_next_certificate_reset(true); + Task::none() + } + Message::CancelCertificateReset => { + self.respond_to_next_certificate_reset(false); + Task::none() + } Message::ShowWindow => { crate::macos_app::set_main_window_visible(true); if let Some(id) = self.main_window { @@ -324,6 +359,7 @@ impl Impactor { } Message::HideWindow => { if let Some(id) = self.main_window { + self.cancel_pending_certificate_resets(); self.main_window = None; crate::macos_app::set_main_window_visible(false); window::close(id) @@ -722,6 +758,7 @@ impl Impactor { }; let tray_menu_refresh_subscription = subscriptions::tray_menu_refresh_subscription(); + let certificate_reset_subscription = subscriptions::certificate_reset_subscription(); let relaunch_subscription = subscriptions::relaunch_subscription(); let close_subscription = iced::event::listen_with(|event, _status, _id| { @@ -737,14 +774,13 @@ impl Impactor { hover_subscription, progress_subscription, tray_menu_refresh_subscription, + certificate_reset_subscription, relaunch_subscription, close_subscription, ]) } pub fn view(&self, window_id: window::Id) -> Element<'_, Message> { - use iced::widget::{column, container}; - if let Some(login_window) = self.login_windows.get(&window_id) { return login_window .view() @@ -754,10 +790,16 @@ impl Impactor { let has_device = self.selected_device.is_some(); let screen_content = self.view_current_screen(has_device); let top_bar = self.view_top_bar(); + let base: Element<'_, Message> = + container(column(vec![top_bar, screen_content]).spacing(appearance::THEME_PADDING)) + .padding(appearance::THEME_PADDING) + .into(); - container(column(vec![top_bar, screen_content]).spacing(appearance::THEME_PADDING)) - .padding(appearance::THEME_PADDING) - .into() + if self.certificate_reset_queue.front().is_some() { + stack![base, self.view_certificate_reset_prompt()].into() + } else { + base + } } fn view_current_screen(&self, has_device: bool) -> Element<'_, Message> { @@ -815,6 +857,57 @@ impl Impactor { .into() } + fn view_certificate_reset_prompt(&self) -> Element<'_, Message> { + let Some(request) = self.certificate_reset_queue.front() else { + return container(text("")).into(); + }; + + let actions = row![ + button(text("Cancel")) + .on_press(Message::CancelCertificateReset) + .style(appearance::s_button), + button(text("Continue")) + .on_press(Message::ConfirmCertificateReset) + .style(appearance::p_button), + ] + .spacing(appearance::THEME_PADDING); + + let dialog = container( + column![ + text("Certificate reset required").size(appearance::THEME_FONT_SIZE + 2.0), + text(&request.message), + actions, + ] + .spacing(appearance::THEME_PADDING), + ) + .padding(appearance::THEME_PADDING * 2.0) + .max_width(420.0) + .style(|theme: &iced::Theme| container::Style { + background: Some(iced::Background::Color(theme.palette().background)), + border: iced::Border { + width: 1.0, + color: theme.palette().warning, + radius: appearance::THEME_CORNER_RADIUS.into(), + }, + ..Default::default() + }); + + container(dialog) + .width(Fill) + .height(Fill) + .center(Fill) + .style(|_theme: &iced::Theme| container::Style { + background: Some(iced::Background::Color(iced::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.45, + })), + ..Default::default() + }) + .into() + } + fn navigate_to_screen(&mut self, screen_type: ImpactorScreenType) { match screen_type { ImpactorScreenType::Main => { diff --git a/apps/plumeimpactor/src/subscriptions.rs b/apps/plumeimpactor/src/subscriptions.rs index 87fb040..89451e1 100644 --- a/apps/plumeimpactor/src/subscriptions.rs +++ b/apps/plumeimpactor/src/subscriptions.rs @@ -148,6 +148,28 @@ pub(crate) fn tray_menu_refresh_subscription() -> Subscription { }) } +pub(crate) fn certificate_reset_subscription() -> Subscription { + Subscription::run(|| { + iced::stream::channel( + 10, + |mut output: iced::futures::channel::mpsc::Sender| async move { + use iced::futures::{SinkExt, StreamExt}; + let (tx, mut rx) = iced::futures::channel::mpsc::unbounded::(); + + std::thread::spawn(move || { + while let Some(request) = crate::certificate_reset::wait_for_request() { + let _ = tx.unbounded_send(Message::CertificateResetRequested(request)); + } + }); + + while let Some(message) = rx.next().await { + let _ = output.send(message).await; + } + }, + ) + }) +} + #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] pub(crate) fn relaunch_subscription() -> Subscription { Subscription::run(|| { @@ -309,12 +331,17 @@ pub(crate) async fn run_installation( team_id }; + let mut on_certificate_reset = || { + send(crate::certificate_reset::WARNING.to_string(), 20); + crate::certificate_reset::confirm() + }; let identity = CertificateIdentity::new_with_session( &session, crate::defaults::get_data_path(), None, team_id, false, + Some(&mut on_certificate_reset), ) .await .map_err(|e| e.to_string())?; @@ -569,12 +596,14 @@ pub(crate) async fn export_certificate(account: plume_store::GsaAccount) -> Resu team_id }; + let mut on_certificate_reset = crate::certificate_reset::confirm; let identity = CertificateIdentity::new_with_session( &session, crate::defaults::get_data_path(), None, team_id, true, + Some(&mut on_certificate_reset), ) .await .map_err(|e| e.to_string())?; diff --git a/apps/plumesign/src/commands/sign.rs b/apps/plumesign/src/commands/sign.rs index 485de2b..86a63f7 100644 --- a/apps/plumesign/src/commands/sign.rs +++ b/apps/plumesign/src/commands/sign.rs @@ -93,9 +93,15 @@ pub async fn execute(args: SignArgs) -> Result<()> { } else if args.apple_id { let session = get_authenticated_account().await?; let team_id = teams(&session).await?; - let cert_identity = - CertificateIdentity::new_with_session(&session, get_data_path(), None, &team_id, false) - .await?; + let cert_identity = CertificateIdentity::new_with_session( + &session, + get_data_path(), + None, + &team_id, + false, + None, + ) + .await?; options.mode = SignerMode::Pem; ( diff --git a/crates/plume_core/src/utils/certificate.rs b/crates/plume_core/src/utils/certificate.rs index 1a50bbe..e3eec5a 100644 --- a/crates/plume_core/src/utils/certificate.rs +++ b/crates/plume_core/src/utils/certificate.rs @@ -59,6 +59,7 @@ impl CertificateIdentity { machine_name: Option, team_id: &String, is_export: bool, + on_certificate_reset: Option<&mut dyn FnMut() -> bool>, ) -> Result { let machine_name = machine_name.unwrap_or_else(|| MACHINE_NAME.to_string()); @@ -99,7 +100,13 @@ impl CertificateIdentity { [cert_pem.into_bytes(), key_pem.into_bytes()] } else { let (certificate, priv_key) = identity - .request_new_certificate(session, team_id, &machine_name, certs) + .request_new_certificate( + session, + team_id, + &machine_name, + certs, + on_certificate_reset, + ) .await?; let cert_pem = encode_string( "CERTIFICATE", @@ -115,7 +122,13 @@ impl CertificateIdentity { } } else { let (cert, priv_key) = identity - .request_new_certificate(session, team_id, &machine_name, certs) + .request_new_certificate( + session, + team_id, + &machine_name, + certs, + on_certificate_reset, + ) .await?; let cert_pem = encode_string("CERTIFICATE", LineEnding::LF, cert.cert_content.as_ref()).unwrap(); @@ -254,6 +267,7 @@ impl CertificateIdentity { team_id: &String, machine_name: &String, certs: Vec, + mut on_certificate_reset: Option<&mut dyn FnMut() -> bool>, ) -> Result<(Cert, RsaPrivateKey), Error> { let priv_key = RsaPrivateKey::new(&mut OsRng, 2048)?; let priv_key_der = priv_key.to_pkcs8_der()?; @@ -276,6 +290,7 @@ impl CertificateIdentity { .iter() .map(|c| c.serial_number.clone()) .collect::>(); + let mut warned_about_reset = false; // When we submit a CSR theres a high chance of it failing, at least // on free developer accounts, we put it in a loop so whenever it does @@ -293,6 +308,17 @@ impl CertificateIdentity { // 7460 is for too many certificates (I think) if matches!(&e, Error::DeveloperApi { result_code, .. } if *result_code == 7460) { + if !warned_about_reset { + if let Some(callback) = on_certificate_reset.as_deref_mut() { + if !callback() { + return Err(Error::Certificate( + "Certificate reset cancelled".into(), + )); + } + } + warned_about_reset = true; + } + // Try to revoke certificates from the candidate list let mut revoked_any = false; for cid in &cert_serial_numbers {