From a7c73fc8cf8bf076a31810817780a6336caf0d69 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 15 Aug 2024 18:23:27 +0300 Subject: [PATCH] gpgme: refactor Rust interface, add tests - Refactor gpgme wrapper interface to use more robust reference counting for the gpgme context object - Add SAFETY comments to unsafe {} blocks - Add tests Signed-off-by: Manos Pitsidianakis --- meli/src/mail/compose/gpg.rs | 185 ++++++++- meli/src/state.rs | 2 +- melib/src/gpgme/io.rs | 195 ++++++---- melib/src/gpgme/key.rs | 158 ++++++++ melib/src/gpgme/mod.rs | 703 ++++++++++++++++++++--------------- melib/src/gpgme/tests.rs | 153 ++++++++ 6 files changed, 1022 insertions(+), 374 deletions(-) create mode 100644 melib/src/gpgme/key.rs create mode 100644 melib/src/gpgme/tests.rs diff --git a/meli/src/mail/compose/gpg.rs b/meli/src/mail/compose/gpg.rs index 19cde8c6..b5f0e883 100644 --- a/meli/src/mail/compose/gpg.rs +++ b/meli/src/mail/compose/gpg.rs @@ -55,8 +55,8 @@ impl KeySelection { allow_remote_lookup: ActionFlag, context: &Context, ) -> Result { - use melib::gpgme::*; - let mut ctx = Context::new()?; + use melib::gpgme::{self, *}; + let mut ctx = gpgme::Context::new()?; if local { ctx.set_auto_key_locate(LocateKey::LOCAL)?; } else { @@ -145,7 +145,11 @@ impl Component for KeySelection { } } else { *self = Self::Error { - err: Error::new(format!("No keys found for {}.", pattern)), + err: if pattern.is_empty() { + Error::new("No keys found.") + } else { + Error::new(format!("No keys found for {}.", pattern)) + }, id, } } @@ -267,3 +271,178 @@ impl Default for GpgComposeState { } } } + +#[cfg(test)] +mod tests { + use std::{borrow::Cow, ffi::CString, thread::sleep, time::Duration}; + + use melib::gpgme::{EngineInfo, LocateKey, Protocol}; + use sealed_test::prelude::*; + + use super::*; + + impl KeySelection { + fn new_mock( + secret: bool, + local: bool, + pattern: String, + allow_remote_lookup: ActionFlag, + context: &Context, + ctx: &mut melib::gpgme::Context, + ) -> Result { + if local { + ctx.set_auto_key_locate(LocateKey::LOCAL)?; + } else { + ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?; + } + let job = ctx.keylist(secret, Some(pattern.clone()))?; + let handle = context.main_loop_handler.job_executor.spawn( + "gpg::keylist".into(), + job, + IsAsync::Blocking, + ); + let mut progress_spinner = ProgressSpinner::new(8, context); + progress_spinner.start(); + Ok(Self::LoadingKeys { + handle, + secret, + local, + pattern, + allow_remote_lookup, + progress_spinner, + }) + } + } + + const PUBKEY: &[u8]=b"-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: GnuPG v2.1.0-gitb3c71eb (GNU/Linux)\r\n\r\nmQGiBDo41NoRBADSfQazKGYf8nokq6zUKH/6INtV6MypSzSGmX2XErnARkIIPPYj\r\ncQRQ8zCbGV7ZU2ezVbzhFLUSJveE8PZUzzCrLp1O2NSyBTRcR5HVSXW95nJfY8eV\r\npOvZRAKul0BVLh81kYTsrfzaaCjh9VWNP26LoeN2r+PjZyktXe7gM3C4SwCgoTxK\r\nWUVi9HoT2HCLY7p7oig5hEcEALdCJal0UYomX3nJapIVLVZg3vkidr1RICYMb2vz\r\n58i17h8sxEtobD1vdIKNejulntaRAXs4n0tDYD9z7pRlwG1CLz1R9WxYzeOOqUDr\r\nfnVXdmU8L/oVWABat8v1V7QQhjMMf+41fuzVwDMMGqjVPLhu4X6wp3A8uyM3YDnQ\r\nVMN1A/4n2G5gHoOvjqxn8Ch5tBAdMGfO8gH4RjQOwzm2R1wPQss/yzUN1+tlMZGX\r\nK2dQ2FCWC/hDUSNaEQRlI15wxxBNZ2RQwlzE2A8v113DpvyzOtv0QO95gJ1teCXC\r\n7j/BN9asgHaBBc39JLO/TcpuI7Hf8PQ5VcP2F0UE3lczGhXbLLRESm9lIFJhbmRv\r\nbSBIYWNrZXIgKHRlc3Qga2V5IHdpdGggcGFzc3BocmFzZSAiYWJjIikgPGpvZUBl\r\neGFtcGxlLmNvbT6IYgQTEQIAIgUCTbdXqQIbIwYLCQgHAwIGFQgCCQoLBBYCAwEC\r\nHgECF4AACgkQr4IkT5zZ/VUcCACfQvSPi//9/gBv8SVrK6O4DiyD+jAAn3LEnfF1\r\n4j6MjwlqXTqol2VgQn1yuQENBDo41N0QBACedJb7Qhm50JSPe1V+rSZKLHT5nc3l\r\n2k1n7//wNsJkgDW2J7snIRjGtSzeNxMPh+hVzFidzAf3sbOlARQoBrMPPKpnJWtm\r\n6LEDf2lSwO36l0/bo6qDRmiFRJoHWytTJEjxVwRclVt4bXqHfNw9FKhZZbcKeAN2\r\noHgmBVSU6edHdwADBQP+OGAkEG4PcfSb8x191R+wkV/q2hA5Ay9z289Dx2rO28CO\r\n4M2fhhcjSmgr6x0DsrkfESCiG47UGJ169eu+QqJwk3HiF4crGN9rE5+VelBVFtrd\r\nMWkX2rPLGQWyw8iCZKbeH8g/ujmkaLovSmalzDcLe4v1xSLaP7Fnfzit0iIGZAGI\r\nRgQYEQIABgUCOjjU3QAKCRCvgiRPnNn9VVSaAJ9+rj1lIQnRl20i8Rom2Hwbe3re\r\n9QCfSYFnkZUw0yKF2DfCfqrDzdGAsbaIRgQYEQIABgUCOjjU3gAKCRCvgiRPnNn9\r\nVe4iAJ9FrGMlFR7s+GWf1scTeeyrthKrPQCfSpc/Yps72aFI7hPfyIa9MuerVZ4=\r\n=QRit\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"; + + #[sealed_test] + fn test_gpg_verify_sig() { + let tempdir = tempfile::tempdir().unwrap(); + { + #[allow(unused_unsafe)] + unsafe { + std::env::set_var("GNUPGHOME", tempdir.path()); + } + + #[allow(unused_unsafe)] + unsafe { + std::env::set_var("GPG_AGENT_INFO", ""); + } + } + + let mut ctx = Context::new_mock(&tempdir); + let mut gpgme_ctx = match melib::gpgme::Context::new() { + Ok(v) => v, + Err(err) if err.kind.is_not_found() => { + eprintln!("INFO: libgpgme could not be loaded, skipping this test."); + return; + } + err => err.unwrap(), + }; + let current_engine_info = gpgme_ctx.engine_info().unwrap(); + let prev_len = current_engine_info.len(); + let Some(EngineInfo { + file_name: Some(engine_file_name), + .. + }) = current_engine_info + .into_iter() + .find(|eng| eng.protocol == Protocol::OpenPGP) + else { + eprintln!("WARN: No openpg protocol engine returned from gpgme."); + return; + }; + gpgme_ctx + .set_engine_info( + Protocol::OpenPGP, + Some(Cow::Owned(CString::new(engine_file_name).unwrap())), + Some(Cow::Owned( + CString::new(tempdir.path().display().to_string()).unwrap(), + )), + ) + .unwrap(); + let new_engine_info = gpgme_ctx.engine_info().unwrap(); + assert_eq!( + new_engine_info.len(), + prev_len, + "new_engine_info was expected to have {} entry/ies but has {}: {:#?}", + prev_len, + new_engine_info.len(), + new_engine_info + ); + assert_eq!( + new_engine_info[0].home_dir, + Some(tempdir.path().display().to_string()), + "new_engine_info was expected to have temp dir as home_dir but has: {:#?}", + new_engine_info[0].home_dir + ); + let mut pubkey_data = Some(gpgme_ctx.new_data_mem(PUBKEY).unwrap()); + for _ in 0..2 { + let mut key_sel = KeySelection::new_mock( + false, + true, + "".to_string(), + false.into(), + &ctx, + &mut gpgme_ctx, + ) + .unwrap(); + let component_id = key_sel.id(); + + for _ in 0..2 { + sleep(Duration::from_secs(2)); + } + while let Ok(ev) = ctx.receiver.try_recv() { + // if !matches!(ev, ThreadEvent::UIEvent(UIEvent::Timer(_))) { + // dbg!(&ev); + // } + if let ThreadEvent::UIEvent(mut ev) = ev { + key_sel.process_event(&mut ev, &mut ctx); + } else if let ThreadEvent::JobFinished(job_id) = ev { + let mut ev = UIEvent::StatusEvent(StatusEvent::JobFinished(job_id)); + key_sel.process_event(&mut ev, &mut ctx); + } + } + if let Some(pubkey_data) = pubkey_data.take() { + assert!( + matches!( + key_sel, + KeySelection::Error { + ref id, + ref err + } if *id == component_id && err.to_string() == melib::Error::new("No keys found.").to_string(), + ), + "key_sel should have been an error but is: {:?}", + key_sel + ); + gpgme_ctx.import_key(pubkey_data).unwrap(); + } else { + let assert_key = |key: &melib::gpgme::Key| { + key.fingerprint() == "ADAB7FCC1F4DE2616ECFA402AF82244F9CD9FD55" + && key.primary_uid() + == Some(melib::Address::new( + Some("Joe Random Hacker".into()), + "joe@example.com".into(), + )) + && key.can_encrypt() + && key.can_sign() + && !key.secret() + && !key.revoked() + && !key.expired() + && !key.invalid() + }; + assert!( + matches!( + key_sel, + KeySelection::Loaded { + ref keys, + widget: _, + } if keys.len() == 1 && assert_key(&keys[0]), + ), + "key_sel should have been an error but is: {:?}", + key_sel + ); + } + } + } +} diff --git a/meli/src/state.rs b/meli/src/state.rs index 671ac68a..7e7c4603 100644 --- a/meli/src/state.rs +++ b/meli/src/state.rs @@ -143,7 +143,7 @@ pub struct Context { pub realized: IndexMap>, pub unrealized: IndexSet, pub main_loop_handler: MainLoopHandler, - receiver: Receiver, + pub receiver: Receiver, input_thread: InputHandler, current_dir: PathBuf, /// Children processes diff --git a/melib/src/gpgme/io.rs b/melib/src/gpgme/io.rs index e1ff2e44..a40be3fb 100644 --- a/melib/src/gpgme/io.rs +++ b/melib/src/gpgme/io.rs @@ -19,97 +19,164 @@ * along with meli. If not, see . */ -use std::io::{self, Read, Seek, Write}; +use std::{ + ffi::{c_int, c_void}, + io::{self, Read, Seek, Write}, + mem::ManuallyDrop, + ptr::NonNull, +}; use super::*; +#[derive(Debug)] #[repr(C)] struct TagData { idx: usize, - fd: ::std::os::raw::c_int, + fd: c_int, io_state: Arc>, } +/// Wrapper type to automatically leak iostate Arc on drop. +#[repr(transparent)] +struct IoStateWrapper(ManuallyDrop>>); + +impl IoStateWrapper { + // SAFETY: `ptr` must be the iostate reference that was leaked in + // `Context::new`. + unsafe fn from_raw(ptr: *mut c_void) -> Self { + let io_state: Arc> = + unsafe { Arc::from_raw(ptr.cast_const().cast::>()) }; + Self(ManuallyDrop::new(io_state)) + } +} + +impl Drop for IoStateWrapper { + fn drop(&mut self) { + // SAFETY: struct unit value is ManuallyDrop, so no Drop impls are called on the + // uninit value. + let inner = unsafe { ManuallyDrop::take(&mut self.0) }; + let _ = Arc::into_raw(inner); + } +} + /// /// # Safety -/// . +/// +/// Must only be used if `add_priv` in `gpgme_io_cbs` is `Arc>`. pub unsafe extern "C" fn gpgme_register_io_cb( - data: *mut ::std::os::raw::c_void, - fd: ::std::os::raw::c_int, - dir: ::std::os::raw::c_int, + data: *mut c_void, + fd: c_int, + dir: c_int, fnc: gpgme_io_cb_t, - fnc_data: *mut ::std::os::raw::c_void, - tag: *mut *mut ::std::os::raw::c_void, + fnc_data: *mut c_void, + tag: *mut *mut c_void, ) -> gpgme_error_t { - let io_state: Arc> = unsafe { Arc::from_raw(data as *const _) }; - let io_state_copy = io_state.clone(); - let mut io_state_lck = io_state.lock().unwrap(); - let idx = io_state_lck.max_idx; - io_state_lck.max_idx += 1; - let (sender, receiver) = smol::channel::unbounded(); - let gpgfd = GpgmeFd { - fd, - fnc, - fnc_data, - idx, - write: dir == 0, - sender, - receiver, - io_state: io_state_copy.clone(), - }; - let tag_data = Arc::into_raw(Arc::new(TagData { - idx, - fd, - io_state: io_state_copy, - })); - unsafe { std::ptr::write(tag, tag_data as *mut _) }; - io_state_lck.ops.insert(idx, gpgfd); - drop(io_state_lck); - let _ = Arc::into_raw(io_state); + // SAFETY: This is the iostate reference that was leaked in `Context::new`. + let io_state: IoStateWrapper = unsafe { IoStateWrapper::from_raw(data) }; + let io_state_copy = Arc::clone(&io_state.0); + if let Ok(mut io_state_lck) = io_state.0.lock() { + let idx = io_state_lck.max_idx; + io_state_lck.max_idx += 1; + let (sender, receiver) = smol::channel::unbounded(); + let gpgfd = GpgmeFd { + fd, + fnc, + fnc_data, + idx, + write: dir == 0, + sender, + receiver, + io_state: io_state_copy.clone(), + }; + { + let tag_data = Arc::into_raw(Arc::new(TagData { + idx, + fd, + io_state: io_state_copy, + })); + // SAFETY: tag_data is a valid pointer from the caller by contract, the tag_data + // allocation is leaked and will only be accessed when the cb is removed. + unsafe { std::ptr::write(tag, tag_data.cast_mut().cast::()) }; + } + io_state_lck.ops.insert(idx, gpgfd); + } 0 } /// /// # Safety -/// . -pub unsafe extern "C" fn gpgme_remove_io_cb(tag: *mut ::std::os::raw::c_void) { - let tag_data: Arc = unsafe { Arc::from_raw(tag as *const _) }; - let mut io_state_lck = tag_data.io_state.lock().unwrap(); - let fd = io_state_lck.ops.remove(&tag_data.idx).unwrap(); - fd.sender.try_send(()).unwrap(); - drop(io_state_lck); - let _ = Arc::into_raw(tag_data); +/// +/// The callback must have been registered with a `TagData` value, therefore +/// `tag` can only hold a valid `Arc` allocation. We assume that gpgme +/// will only call the remove cb with this tag data once. +pub unsafe extern "C" fn gpgme_remove_io_cb(tag: *mut c_void) { + let tag_data: Arc = unsafe { Arc::from_raw(tag.cast_const().cast::()) }; + let gpgfd: GpgmeFd = { + let Ok(mut io_state_lck) = tag_data.io_state.lock() else { + // mutex is poisoned, bail out. + return; + }; + io_state_lck.ops.remove(&tag_data.idx).unwrap_or_else(|| { + panic!( + "gpgme_remove_io_cb called with tag_data {:?}, but idx {} is not included in \ + io_state ops. This is a bug.", + tag_data, tag_data.idx + ) + }) + }; + _ = gpgfd.sender.try_send(()); } /// /// # Safety -/// . +/// +/// Must only be used if `add_priv` in `gpgme_io_cbs` is `Arc>`. pub unsafe extern "C" fn gpgme_event_io_cb( - data: *mut ::std::os::raw::c_void, - type_: gpgme_event_io_t, - type_data: *mut ::std::os::raw::c_void, + data: *mut c_void, + r#type: gpgme_event_io_t, + type_data: *mut c_void, ) { - if type_ == gpgme_event_io_t_GPGME_EVENT_DONE { - let err = type_data as gpgme_io_event_done_data_t; - let io_state: Arc> = unsafe { Arc::from_raw(data as *const _) }; - let io_state_lck = io_state.lock().unwrap(); - io_state_lck.sender.try_send(()).unwrap(); - *io_state_lck.done.lock().unwrap() = - Some(gpgme_error_try(&io_state_lck.lib, unsafe { (*err).err })); - drop(io_state_lck); - let _ = Arc::into_raw(io_state); - } else if type_ == gpgme_event_io_t_GPGME_EVENT_NEXT_KEY { - if let Some(inner) = std::ptr::NonNull::new(type_data as gpgme_key_t) { - let io_state: Arc> = unsafe { Arc::from_raw(data as *const _) }; - let io_state_lck = io_state.lock().unwrap(); - io_state_lck - .key_sender - .try_send(KeyInner { inner }) - .unwrap(); - drop(io_state_lck); - let _ = Arc::into_raw(io_state); + if r#type == gpgme_event_io_t_GPGME_EVENT_START { + return; + } + + // SAFETY: This is the iostate reference that was leaked in `Context::new`. + let io_state: IoStateWrapper = unsafe { IoStateWrapper::from_raw(data) }; + + if r#type == gpgme_event_io_t_GPGME_EVENT_DONE { + let Some(status) = NonNull::new(type_data.cast::()) else { + log::error!("gpgme_event_io_cb DONE event with NULL type_data. This is a gpgme bug.",); + return; + }; + // SAFETY: since type is DONE and type_data is not NULL, status is a valid + // gpgme_io_event_done_data pointer. + let err = unsafe { status.as_ref().err }; + if let Ok(io_state_lck) = io_state.0.lock() { + _ = io_state_lck.sender.try_send(()); + if let Ok(mut done) = io_state_lck.done.lock() { + *done = Some(gpgme_error_try(&io_state_lck.lib, err)); + } + } + return; + } + + if r#type == gpgme_event_io_t_GPGME_EVENT_NEXT_KEY { + let Some(ptr) = NonNull::new(type_data.cast::<_gpgme_key>()) else { + log::error!( + "gpgme_event_io_cb NEXT_KEY event with NULL type_data. This is a gpgme bug.", + ); + return; + }; + if let Ok(io_state_lck) = io_state.0.lock() { + _ = io_state_lck.key_sender.try_send(KeyInner { ptr }); } + return; } + + log::error!( + "gpgme_event_io_cb called with unexpected event type: {}", + r#type + ); } impl Read for Data { diff --git a/melib/src/gpgme/key.rs b/melib/src/gpgme/key.rs new file mode 100644 index 00000000..428e77cf --- /dev/null +++ b/melib/src/gpgme/key.rs @@ -0,0 +1,158 @@ +/* + * melib - gpgme module + * + * Copyright 2020 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +// [ref:DEBT] segfaults on libgpgme code can crash the entire app; it should be +// handled. + +use std::{borrow::Cow, ffi::CStr, ptr::NonNull, sync::Arc}; + +use super::bindings::*; +use crate::email::Address; + +#[derive(Clone)] +pub struct KeyInner { + pub ptr: NonNull<_gpgme_key>, +} + +unsafe impl Send for KeyInner {} +unsafe impl Sync for KeyInner {} + +pub struct Key { + pub inner: KeyInner, + pub lib: Arc, +} +unsafe impl Send for Key {} +unsafe impl Sync for Key {} + +impl Clone for Key { + fn clone(&self) -> Self { + let lib = self.lib.clone(); + unsafe { + call!(&self.lib, gpgme_key_ref)(self.inner.ptr.as_ptr()); + } + Self { + inner: self.inner.clone(), + lib, + } + } +} + +impl Key { + #[inline] + pub fn new(inner: KeyInner, lib: Arc) -> Self { + Self { inner, lib } + } + + pub fn primary_uid(&self) -> Option
{ + unsafe { + if (self.inner.ptr.as_ref()).uids.is_null() { + return None; + } + let uid = self.inner.ptr.as_ref().uids; + if (*uid).name.is_null() && (*uid).email.is_null() { + None + } else if (*uid).name.is_null() { + Some(Address::new( + None, + CStr::from_ptr((*uid).email).to_string_lossy().to_string(), + )) + } else if (*uid).email.is_null() { + Some(Address::new( + None, + CStr::from_ptr((*uid).name).to_string_lossy().to_string(), + )) + } else { + Some(Address::new( + Some(CStr::from_ptr((*uid).name).to_string_lossy().to_string()), + CStr::from_ptr((*uid).email).to_string_lossy().to_string(), + )) + } + } + } + + pub fn revoked(&self) -> bool { + unsafe { self.inner.ptr.as_ref().revoked() > 0 } + } + + pub fn expired(&self) -> bool { + unsafe { self.inner.ptr.as_ref().expired() > 0 } + } + + pub fn disabled(&self) -> bool { + unsafe { self.inner.ptr.as_ref().disabled() > 0 } + } + + pub fn invalid(&self) -> bool { + unsafe { self.inner.ptr.as_ref().invalid() > 0 } + } + + pub fn can_encrypt(&self) -> bool { + unsafe { self.inner.ptr.as_ref().can_encrypt() > 0 } + } + + pub fn can_sign(&self) -> bool { + unsafe { self.inner.ptr.as_ref().can_sign() > 0 } + } + + pub fn secret(&self) -> bool { + unsafe { self.inner.ptr.as_ref().secret() > 0 } + } + + pub fn fingerprint(&self) -> Cow<'_, str> { + // SAFETY: self.inner.ptr is valid. + let fpr = unsafe { self.inner.ptr.as_ref().fpr }; + let Some(fpr_pr) = NonNull::new(fpr) else { + return Cow::Borrowed(""); + }; + unsafe { CStr::from_ptr(fpr_pr.as_ptr()) }.to_string_lossy() + } +} + +impl std::fmt::Debug for Key { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.debug_struct(crate::identify!(Key)) + .field("fingerprint", &self.fingerprint()) + .field("uid", &self.primary_uid()) + .field("can_encrypt", &self.can_encrypt()) + .field("can_sign", &self.can_sign()) + .field("secret", &self.secret()) + .field("revoked", &self.revoked()) + .field("expired", &self.expired()) + .field("invalid", &self.invalid()) + .finish_non_exhaustive() + } +} + +impl PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + self.fingerprint() == other.fingerprint() + } +} + +impl Eq for Key {} + +impl Drop for Key { + fn drop(&mut self) { + unsafe { + call!(&self.lib, gpgme_key_unref)(self.inner.ptr.as_ptr()); + } + } +} diff --git a/melib/src/gpgme/mod.rs b/melib/src/gpgme/mod.rs index d17afbde..788630f8 100644 --- a/melib/src/gpgme/mod.rs +++ b/melib/src/gpgme/mod.rs @@ -25,15 +25,17 @@ use std::{ borrow::Cow, collections::HashMap, - ffi::{CStr, CString, OsStr}, + ffi::{c_void, CStr, CString, OsStr}, future::Future, io::Seek, + mem::ManuallyDrop, os::unix::{ ffi::OsStrExt, io::{AsRawFd, RawFd}, }, path::Path, pin::Pin, + ptr::NonNull, sync::{Arc, Mutex}, }; @@ -42,13 +44,13 @@ use serde::{ de::{self, Deserialize}, Deserializer, Serialize, Serializer, }; -use smol::Async; +use smol::{ + channel::{Receiver, Sender}, + Async, +}; use crate::{ - email::{ - pgp::{DecryptionMetadata, Recipient}, - Address, - }, + email::pgp::{DecryptionMetadata, Recipient}, error::{Error, ErrorKind, Result, ResultIntoError}, }; @@ -64,26 +66,31 @@ macro_rules! call { }}; } -macro_rules! c_string_literal { - ($lit:literal) => {{ - unsafe { - CStr::from_bytes_with_nul_unchecked(concat!($lit, "\0").as_bytes()).as_ptr() - as *const ::std::os::raw::c_char - } - }}; -} pub mod bindings; +#[cfg(test)] +mod tests; use bindings::*; +pub mod key; +pub use key::*; pub mod io; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum GpgmeFlag { - ///"auto-key-retrieve" + /// "auto-key-retrieve" AutoKeyRetrieve, OfflineMode, AsciiArmor, } +impl GpgmeFlag { + // SAFETY: Value is NULL terminated. + const AUTO_KEY_RETRIEVE: &'static CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(b"auto-key-retrieve\0") }; + // SAFETY: Value is NULL terminated. + const AUTO_KEY_LOCATE: &'static CStr = + unsafe { CStr::from_bytes_with_nul_unchecked(b"auto-key-locate\0") }; +} + bitflags! { #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct LocateKey: u8 { @@ -186,14 +193,17 @@ impl std::fmt::Display for LocateKey { } } +type Done = Arc>>>; + +#[derive(Debug)] struct IoState { max_idx: usize, ops: HashMap, - done: Arc>>>, - sender: smol::channel::Sender<()>, - receiver: smol::channel::Receiver<()>, - key_sender: smol::channel::Sender, - key_receiver: smol::channel::Receiver, + done: Done, + sender: Sender<()>, + receiver: Receiver<()>, + key_sender: Sender, + key_receiver: Receiver, lib: Arc, } @@ -201,32 +211,40 @@ unsafe impl Send for IoState {} unsafe impl Sync for IoState {} pub struct ContextInner { - inner: std::ptr::NonNull, + ptr: NonNull, lib: Arc, } unsafe impl Send for ContextInner {} unsafe impl Sync for ContextInner {} +#[derive(Clone)] pub struct Context { inner: Arc, - io_state: Arc>, + io_state: Arc, } -unsafe impl Send for Context {} -unsafe impl Sync for Context {} - impl Drop for ContextInner { #[inline] fn drop(&mut self) { - unsafe { call!(self.lib, gpgme_release)(self.inner.as_mut()) } + unsafe { call!(self.lib, gpgme_release)(self.ptr.as_mut()) } } } impl Context { pub fn new() -> Result { - let lib = - Arc::new(unsafe { libloading::Library::new(libloading::library_filename("gpgme")) }?); + let lib = Arc::new( + match unsafe { libloading::Library::new(libloading::library_filename("gpgme")) } { + Ok(v) => v, + Err(err) => { + let source = Error::from(err).set_kind(ErrorKind::LinkedLibrary("gpgme")); + let mut err = + Error::new("Could not use libgpgme").set_kind(ErrorKind::NotFound); + err.source = Some(Box::new(source)); + return Err(err); + } + }, + ); if unsafe { call!(&lib, gpgme_check_version)(GPGME_VERSION.as_bytes().as_ptr()) }.is_null() { return Err(Error::new(format!( @@ -237,13 +255,14 @@ impl Context { .to_string_lossy() }, )) - .set_kind(ErrorKind::External)); + .set_kind(ErrorKind::LinkedLibrary("gpgme"))); }; let (sender, receiver) = smol::channel::unbounded(); let (key_sender, key_receiver) = smol::channel::unbounded(); let mut ptr = std::ptr::null_mut(); - let io_state = Arc::new(Mutex::new(IoState { + + let (io_state, mut io_cbs) = IoStateWrapper::new(IoState { max_idx: 0, ops: HashMap::default(), done: Arc::new(Mutex::new(None)), @@ -252,17 +271,7 @@ impl Context { key_sender, key_receiver, lib: lib.clone(), - })); - let add_priv_data = io_state.clone(); - let event_priv_data = io_state.clone(); - - let mut io_cbs = gpgme_io_cbs { - add: Some(io::gpgme_register_io_cb), - add_priv: Arc::into_raw(add_priv_data) as *mut ::std::os::raw::c_void, /* add_priv: *mut ::std::os::raw::c_void, */ - remove: Some(io::gpgme_remove_io_cb), - event: Some(io::gpgme_event_io_cb), - event_priv: Arc::into_raw(event_priv_data) as *mut ::std::os::raw::c_void, /* pub event_priv: *mut ::std::os::raw::c_void, */ - }; + }); unsafe { gpgme_error_try(&lib, call!(&lib, gpgme_new)(&mut ptr))?; @@ -270,8 +279,14 @@ impl Context { } let mut ret = Self { inner: Arc::new(ContextInner { - inner: std::ptr::NonNull::new(ptr) - .ok_or_else(|| Error::new("Could not use libgpgme").set_kind(ErrorKind::Bug))?, + ptr: NonNull::new(ptr).ok_or_else(|| { + Error::new("Could not use libgpgme") + .set_details( + "gpgme_new + returned a NULL value.", + ) + .set_kind(ErrorKind::LinkedLibrary("gpgme")) + })?, lib, }), io_state, @@ -283,18 +298,14 @@ impl Context { Ok(ret) } - fn set_flag_inner( - &self, - raw_flag: *const ::std::os::raw::c_char, - raw_value: *const ::std::os::raw::c_char, - ) -> Result<()> { + fn set_flag_inner(&self, raw_flag: &'static CStr, raw_value: &CStr) -> Result<()> { unsafe { gpgme_error_try( &self.inner.lib, call!(&self.inner.lib, gpgme_set_ctx_flag)( - self.inner.inner.as_ptr(), - raw_flag, - raw_value, + self.inner.ptr.as_ptr(), + raw_flag.as_ptr(), + raw_value.as_ptr(), ), )?; } @@ -307,7 +318,7 @@ impl Context { GpgmeFlag::OfflineMode => { unsafe { call!(&self.inner.lib, gpgme_set_offline)( - self.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), if value { 1 } else { 0 }, ); }; @@ -316,44 +327,42 @@ impl Context { GpgmeFlag::AsciiArmor => { unsafe { call!(&self.inner.lib, gpgme_set_armor)( - self.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), if value { 1 } else { 0 }, ); }; return Ok(self); } }; - const VALUE_ON: &[u8; 2] = b"1\0"; - const VALUE_OFF: &[u8; 2] = b"0\0"; + // SAFETY: Value is NULL terminated. + const VALUE_ON: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"1\0") }; + // SAFETY: Value is NULL terminated. + const VALUE_OFF: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"0\0") }; let raw_flag = match flag { - GpgmeFlag::AutoKeyRetrieve => c_string_literal!("auto-key-retrieve"), + GpgmeFlag::AutoKeyRetrieve => GpgmeFlag::AUTO_KEY_RETRIEVE, GpgmeFlag::AsciiArmor | GpgmeFlag::OfflineMode => unreachable!(), }; - self.set_flag_inner( - raw_flag, - if value { VALUE_ON } else { VALUE_OFF }.as_ptr() as *const ::std::os::raw::c_char, - )?; + self.set_flag_inner(raw_flag, if value { VALUE_ON } else { VALUE_OFF })?; Ok(self) } - fn get_flag_inner( - &self, - raw_flag: *const ::std::os::raw::c_char, - ) -> *const ::std::os::raw::c_char { - unsafe { call!(&self.inner.lib, gpgme_get_ctx_flag)(self.inner.inner.as_ptr(), raw_flag) } + fn get_flag_inner(&self, raw_flag: &'static CStr) -> *const ::std::os::raw::c_char { + unsafe { + call!(&self.inner.lib, gpgme_get_ctx_flag)(self.inner.ptr.as_ptr(), raw_flag.as_ptr()) + } } pub fn get_flag(&self, flag: GpgmeFlag) -> Result { let raw_flag = match flag { - GpgmeFlag::AutoKeyRetrieve => c_string_literal!("auto-key-retrieve"), + GpgmeFlag::AutoKeyRetrieve => GpgmeFlag::AUTO_KEY_RETRIEVE, GpgmeFlag::OfflineMode => { return Ok(unsafe { - call!(&self.inner.lib, gpgme_get_offline)(self.inner.inner.as_ptr()) > 0 + call!(&self.inner.lib, gpgme_get_offline)(self.inner.ptr.as_ptr()) > 0 }); } GpgmeFlag::AsciiArmor => { return Ok(unsafe { - call!(&self.inner.lib, gpgme_get_armor)(self.inner.inner.as_ptr()) > 0 + call!(&self.inner.lib, gpgme_get_armor)(self.inner.ptr.as_ptr()) > 0 }); } }; @@ -361,26 +370,28 @@ impl Context { Ok(!val.is_null()) } - pub fn set_auto_key_locate(&self, val: LocateKey) -> Result<()> { - let auto_key_locate: *const ::std::os::raw::c_char = c_string_literal!("auto-key-locate"); + pub fn set_auto_key_locate(&mut self, val: LocateKey) -> Result<&mut Self> { if val == LocateKey::NODEFAULT { - self.set_flag_inner(auto_key_locate, c_string_literal!("clear,nodefault")) + self.set_flag_inner( + GpgmeFlag::AUTO_KEY_LOCATE, + // SAFETY: Value is NULL terminated. + unsafe { CStr::from_bytes_with_nul_unchecked(b"clear,nodefault\0") }, + )?; } else { let mut accum = val.to_string(); accum.push('\0'); self.set_flag_inner( - auto_key_locate, + GpgmeFlag::AUTO_KEY_LOCATE, CStr::from_bytes_with_nul(accum.as_bytes()) - .map_err(|err| format!("Expected `{}`: {}", accum.as_str(), err))? - .as_ptr() as *const _, - ) + .map_err(|err| format!("Expected `{}`: {}", accum.as_str(), err))?, + )?; } + Ok(self) } pub fn get_auto_key_locate(&self) -> Result { - let auto_key_locate: *const ::std::os::raw::c_char = c_string_literal!("auto-key-locate"); - let raw_value = - unsafe { CStr::from_ptr(self.get_flag_inner(auto_key_locate)) }.to_string_lossy(); + let raw_value = unsafe { CStr::from_ptr(self.get_flag_inner(GpgmeFlag::AUTO_KEY_LOCATE)) } + .to_string_lossy(); let mut val = LocateKey::NODEFAULT; if !raw_value.contains("nodefault") { for mechanism in raw_value.split(',') { @@ -432,7 +443,7 @@ impl Context { lib: self.inner.lib.clone(), kind: DataKind::Memory, bytes, - inner: std::ptr::NonNull::new(ptr).ok_or_else(|| { + inner: NonNull::new(ptr).ok_or_else(|| { Error::new("Could not create libgpgme data").set_kind(ErrorKind::Bug) })?, }) @@ -462,7 +473,7 @@ impl Context { lib: self.inner.lib.clone(), kind: DataKind::Memory, bytes, - inner: std::ptr::NonNull::new(ptr).ok_or_else(|| { + inner: NonNull::new(ptr).ok_or_else(|| { Error::new("Could not create libgpgme data").set_kind(ErrorKind::Bug) })?, }) @@ -477,7 +488,7 @@ impl Context { gpgme_error_try( &self.inner.lib, call!(&self.inner.lib, gpgme_op_verify_start)( - self.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), signature.inner.as_mut(), text.inner.as_mut(), std::ptr::null_mut(), @@ -485,16 +496,8 @@ impl Context { )?; } - let ctx = self.inner.clone(); - let io_state = self.io_state.clone(); - let io_state_lck = self.io_state.lock().unwrap(); - let done = io_state_lck.done.clone(); - let fut = io_state_lck - .ops - .values() - .map(|a| Async::new(a.clone()).unwrap()) - .collect::>>(); - drop(io_state_lck); + let ctx = self.clone(); + let (done, fut) = self.io_state.done_fut()?; Ok(async move { let _s = signature; let _t = text; @@ -545,15 +548,16 @@ impl Context { } })) .await; - debug!("done with fut join"); + log::trace!("done with fut join"); let rcv = { - let io_state_lck = io_state.lock().unwrap(); + let io_state_lck = ctx.io_state.lock().unwrap(); io_state_lck.receiver.clone() }; let _ = rcv.recv().await; { - let verify_result: gpgme_verify_result_t = - unsafe { call!(&ctx.lib, gpgme_op_verify_result)(ctx.inner.as_ptr()) }; + let verify_result: gpgme_verify_result_t = unsafe { + call!(&ctx.inner.lib, gpgme_op_verify_result)(ctx.inner.ptr.as_ptr()) + }; if verify_result.is_null() { return Err(Error::new( "Unspecified libgpgme error: gpgme_op_verify_result returned NULL.", @@ -561,19 +565,16 @@ impl Context { .set_kind(ErrorKind::External)); } } - let io_state_lck = io_state.lock().unwrap(); - let ret = io_state_lck - .done - .lock() - .unwrap() - .take() - .unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error"))); + let io_state_lck = ctx.io_state.lock().unwrap(); + let ret = io_state_lck.done.lock().unwrap().take().unwrap_or_else(|| { + Err(Error::new("Unspecified libgpgme error").set_kind(ErrorKind::Bug)) + }); ret }) } pub fn keylist( - &mut self, + &self, secret: bool, pattern: Option, ) -> Result>>> { @@ -586,7 +587,7 @@ impl Context { gpgme_error_try( &self.inner.lib, call!(&self.inner.lib, gpgme_op_keylist_start)( - self.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), pattern .as_ref() .map(|cs| cs.as_ptr()) @@ -597,16 +598,8 @@ impl Context { )?; } - let ctx = self.inner.clone(); - let io_state = self.io_state.clone(); - let io_state_lck = self.io_state.lock().unwrap(); - let done = io_state_lck.done.clone(); - let fut = io_state_lck - .ops - .values() - .map(|a| Async::new(a.clone()).unwrap()) - .collect::>>(); - drop(io_state_lck); + let ctx = self.clone(); + let (done, fut) = self.io_state.done_fut()?; Ok(async move { futures::future::join_all(fut.iter().map(|fut| { let done = done.clone(); @@ -656,7 +649,7 @@ impl Context { })) .await; let (rcv, key_receiver) = { - let io_state_lck = io_state.lock().unwrap(); + let io_state_lck = ctx.io_state.lock().unwrap(); ( io_state_lck.receiver.clone(), io_state_lck.key_receiver.clone(), @@ -665,11 +658,11 @@ impl Context { let _ = rcv.recv().await; unsafe { gpgme_error_try( - &ctx.lib, - call!(&ctx.lib, gpgme_op_keylist_end)(ctx.inner.as_ptr()), + &ctx.inner.lib, + call!(&ctx.inner.lib, gpgme_op_keylist_end)(ctx.inner.ptr.as_ptr()), )?; } - io_state + ctx.io_state .lock() .unwrap() .done @@ -679,9 +672,10 @@ impl Context { .unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error")))?; let mut keys = vec![]; while let Ok(inner) = key_receiver.try_recv() { - let key = Key::new(inner, ctx.lib.clone()); + let key = Key::new(inner, ctx.inner.lib.clone()); keys.push(key); } + drop(ctx); Ok(keys) }) } @@ -702,13 +696,13 @@ impl Context { &self.inner.lib, call!(&self.inner.lib, gpgme_data_new)(&mut sig), )?; - call!(&self.inner.lib, gpgme_signers_clear)(self.inner.inner.as_ptr()); + call!(&self.inner.lib, gpgme_signers_clear)(self.inner.ptr.as_ptr()); for k in sign_keys { gpgme_error_try( &self.inner.lib, call!(&self.inner.lib, gpgme_signers_add)( - self.inner.inner.as_ptr(), - k.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), + k.inner.ptr.as_ptr(), ), )?; } @@ -718,7 +712,7 @@ impl Context { gpgme_error_try( &self.inner.lib, call!(&self.inner.lib, gpgme_op_sign_start)( - self.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), text.inner.as_mut(), sig, gpgme_sig_mode_t_GPGME_SIG_MODE_DETACH, @@ -729,19 +723,13 @@ impl Context { lib: self.inner.lib.clone(), kind: DataKind::Memory, bytes: Pin::new(vec![]), - inner: std::ptr::NonNull::new(sig) - .ok_or_else(|| Error::new("internal libgpgme error").set_kind(ErrorKind::Bug))?, + inner: NonNull::new(sig).ok_or_else(|| { + Error::new("internal libgpgme error").set_kind(ErrorKind::LinkedLibrary("gpgme")) + })?, }; - let io_state = self.io_state.clone(); - let io_state_lck = self.io_state.lock().unwrap(); - let done = io_state_lck.done.clone(); - let fut = io_state_lck - .ops - .values() - .map(|a| Async::new(a.clone()).unwrap()) - .collect::>>(); - drop(io_state_lck); + let ctx = self.clone(); + let (done, fut) = self.io_state.done_fut()?; Ok(async move { futures::future::join_all(fut.iter().map(|fut| { let done = done.clone(); @@ -791,10 +779,10 @@ impl Context { })) .await; { - let rcv = io_state.lock().unwrap().receiver.clone(); + let rcv = ctx.io_state.lock().unwrap().receiver.clone(); let _ = rcv.recv().await; } - io_state + ctx.io_state .lock() .unwrap() .done @@ -825,7 +813,7 @@ impl Context { gpgme_error_try( &self.inner.lib, call!(&self.inner.lib, gpgme_op_decrypt_start)( - self.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), cipher.inner.as_mut(), plain, ), @@ -835,20 +823,13 @@ impl Context { lib: self.inner.lib.clone(), kind: DataKind::Memory, bytes: Pin::new(vec![]), - inner: std::ptr::NonNull::new(plain) - .ok_or_else(|| Error::new("internal libgpgme error").set_kind(ErrorKind::Bug))?, + inner: NonNull::new(plain).ok_or_else(|| { + Error::new("internal libgpgme error").set_kind(ErrorKind::LinkedLibrary("gpgme")) + })?, }; - let ctx = self.inner.clone(); - let io_state = self.io_state.clone(); - let io_state_lck = self.io_state.lock().unwrap(); - let done = io_state_lck.done.clone(); - let fut = io_state_lck - .ops - .values() - .map(|a| Async::new(a.clone()).unwrap()) - .collect::>>(); - drop(io_state_lck); + let ctx = self.clone(); + let (done, fut) = self.io_state.done_fut()?; Ok(async move { let _c = cipher; futures::future::join_all(fut.iter().map(|fut| { @@ -898,9 +879,9 @@ impl Context { } })) .await; - let rcv = { io_state.lock().unwrap().receiver.clone() }; + let rcv = { ctx.io_state.lock().unwrap().receiver.clone() }; let _ = rcv.recv().await; - io_state + ctx.io_state .lock() .unwrap() .done @@ -910,12 +891,12 @@ impl Context { .unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error")))?; let decrypt_result = - unsafe { call!(&ctx.lib, gpgme_op_decrypt_result)(ctx.inner.as_ptr()) }; + unsafe { call!(&ctx.inner.lib, gpgme_op_decrypt_result)(ctx.inner.ptr.as_ptr()) }; if decrypt_result.is_null() { return Err(Error::new( "Unspecified libgpgme error: gpgme_op_decrypt_result returned NULL.", ) - .set_kind(ErrorKind::External)); + .set_kind(ErrorKind::LinkedLibrary("gpgme"))); } let mut recipients = vec![]; let is_mime; @@ -953,7 +934,7 @@ impl Context { } else { None }, - status: gpgme_error_try(&ctx.lib, (*recipient_iter).status), + status: gpgme_error_try(&ctx.inner.lib, (*recipient_iter).status), }); recipient_iter = (*recipient_iter).next; } @@ -986,7 +967,7 @@ impl Context { ); } unsafe { - call!(&self.inner.lib, gpgme_signers_clear)(self.inner.inner.as_ptr()); + call!(&self.inner.lib, gpgme_signers_clear)(self.inner.ptr.as_ptr()); } let also_sign: bool = if let Some(keys) = sign_keys { @@ -998,8 +979,8 @@ impl Context { gpgme_error_try( &self.inner.lib, call!(&self.inner.lib, gpgme_signers_add)( - self.inner.inner.as_ptr(), - k.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), + k.inner.ptr.as_ptr(), ), )?; } @@ -1011,7 +992,7 @@ impl Context { }; let mut cipher: gpgme_data_t = std::ptr::null_mut(); let mut raw_keys: Vec = Vec::with_capacity(encrypt_keys.len() + 1); - raw_keys.extend(encrypt_keys.iter().map(|k| k.inner.inner.as_ptr())); + raw_keys.extend(encrypt_keys.iter().map(|k| k.inner.ptr.as_ptr())); raw_keys.push(std::ptr::null_mut()); unsafe { gpgme_error_try( @@ -1022,7 +1003,7 @@ impl Context { &self.inner.lib, if also_sign { call!(&self.inner.lib, gpgme_op_encrypt_sign_start)( - self.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), raw_keys.as_mut_ptr(), gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_ENCRYPT_TO | gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_COMPRESS, @@ -1031,7 +1012,7 @@ impl Context { ) } else { call!(&self.inner.lib, gpgme_op_encrypt_start)( - self.inner.inner.as_ptr(), + self.inner.ptr.as_ptr(), raw_keys.as_mut_ptr(), gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_ENCRYPT_TO | gpgme_encrypt_flags_t_GPGME_ENCRYPT_NO_COMPRESS, @@ -1045,20 +1026,13 @@ impl Context { lib: self.inner.lib.clone(), kind: DataKind::Memory, bytes: Pin::new(vec![]), - inner: std::ptr::NonNull::new(cipher) - .ok_or_else(|| Error::new("internal libgpgme error").set_kind(ErrorKind::Bug))?, + inner: NonNull::new(cipher).ok_or_else(|| { + Error::new("internal libgpgme error").set_kind(ErrorKind::LinkedLibrary("gpgme")) + })?, }; - let ctx = self.inner.clone(); - let io_state = self.io_state.clone(); - let io_state_lck = self.io_state.lock().unwrap(); - let done = io_state_lck.done.clone(); - let fut = io_state_lck - .ops - .values() - .map(|a| Async::new(a.clone()).unwrap()) - .collect::>>(); - drop(io_state_lck); + let ctx = self.clone(); + let (done, fut) = self.io_state.done_fut()?; Ok(async move { futures::future::join_all(fut.iter().map(|fut| { let done = done.clone(); @@ -1107,9 +1081,9 @@ impl Context { } })) .await; - let rcv = { io_state.lock().unwrap().receiver.clone() }; + let rcv = { ctx.io_state.lock().unwrap().receiver.clone() }; let _ = rcv.recv().await; - io_state + ctx.io_state .lock() .unwrap() .done @@ -1119,12 +1093,12 @@ impl Context { .unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error")))?; let encrypt_result = - unsafe { call!(&ctx.lib, gpgme_op_encrypt_result)(ctx.inner.as_ptr()) }; + unsafe { call!(&ctx.inner.lib, gpgme_op_encrypt_result)(ctx.inner.ptr.as_ptr()) }; if encrypt_result.is_null() { return Err(Error::new( "Unspecified libgpgme error: gpgme_op_encrypt_result returned NULL.", ) - .set_kind(ErrorKind::External)); + .set_kind(ErrorKind::LinkedLibrary("gpgme"))); } /* Rewind cursor */ cipher @@ -1135,6 +1109,165 @@ impl Context { cipher.into_bytes() }) } + + pub fn engine_info(&self) -> Result> { + let mut ptr: gpgme_engine_info_t = + unsafe { call!(&self.inner.lib, gpgme_ctx_get_engine_info)(self.inner.ptr.as_ptr()) }; + let mut retval = vec![]; + macro_rules! to_s { + ($p:expr) => {{ + if $p.is_null() { + None + } else { + unsafe { Some(CStr::from_ptr($p).to_string_lossy().to_string()) } + } + }}; + } + while let Some(eng) = NonNull::new(ptr) { + let eng_ref = unsafe { eng.as_ref() }; + ptr = eng_ref.next; + retval.push(EngineInfo { + protocol: eng_ref.protocol.into(), + file_name: to_s! {eng_ref.file_name}, + version: to_s! {eng_ref.version}, + req_version: to_s! {eng_ref.req_version}, + home_dir: to_s! {eng_ref.home_dir}, + }); + } + + Ok(retval) + } + + pub fn set_engine_info( + &mut self, + protocol: Protocol, + file_name: Option>, + home_dir: Option>, + ) -> Result<()> { + unsafe { + gpgme_error_try( + &self.inner.lib, + call!(&self.inner.lib, gpgme_ctx_set_engine_info)( + self.inner.ptr.as_ptr(), + protocol as u32, + file_name + .as_ref() + .map(|c| c.as_ptr()) + .unwrap_or_else(std::ptr::null), + home_dir + .as_ref() + .map(|c| c.as_ptr()) + .unwrap_or_else(std::ptr::null), + ), + )?; + } + Ok(()) + } + + pub fn set_protocol(&mut self, protocol: Protocol) -> Result<()> { + unsafe { + gpgme_error_try( + &self.inner.lib, + call!(&self.inner.lib, gpgme_set_protocol)( + self.inner.ptr.as_ptr(), + protocol as u32, + ), + )?; + } + Ok(()) + } + + pub fn import_key(&mut self, mut key_data: Data) -> Result<()> { + unsafe { + gpgme_error_try( + &self.inner.lib, + call!(&self.inner.lib, gpgme_op_import)( + self.inner.ptr.as_ptr(), + key_data.inner.as_mut(), + ), + )?; + } + let result = + unsafe { call!(&self.inner.lib, gpgme_op_import_result)(self.inner.ptr.as_ptr()) }; + if let Some(ptr) = NonNull::new(result) { + let res = unsafe { ptr.as_ref() }; + if res.imported == 0 && res.secret_imported == 0 { + return Err(Error::new("Key was not imported.")); + } + } + Ok(()) + } + + #[cfg(test)] + pub fn set_passphrase_cb( + &mut self, + cb: gpgme_passphrase_cb_t, + data: Option<*mut c_void>, + ) -> Result<()> { + unsafe { + call!(&self.inner.lib, gpgme_get_pinentry_mode)(self.inner.ptr.as_ptr()); + } + unsafe { + gpgme_error_try( + &self.inner.lib, + call!(&self.inner.lib, gpgme_set_pinentry_mode)( + self.inner.ptr.as_ptr(), + if cb.is_none() { + gpgme_pinentry_mode_t_GPGME_PINENTRY_MODE_DEFAULT + } else { + gpgme_pinentry_mode_t_GPGME_PINENTRY_MODE_LOOPBACK + }, + ), + )?; + } + unsafe { + call!(&self.inner.lib, gpgme_set_passphrase_cb)( + self.inner.ptr.as_ptr(), + cb, + data.unwrap_or(std::ptr::null_mut()), + ); + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct EngineInfo { + pub protocol: Protocol, + pub file_name: Option, + pub version: Option, + pub req_version: Option, + pub home_dir: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum Protocol { + OpenPGP = 0, + CMS = 1, + GPGCONF = 2, + ASSUAN = 3, + G13 = 4, + UISERVER = 5, + SPAWN = 6, + DEFAULT = 254, + UNKNOWN = 255, +} + +impl From for Protocol { + fn from(val: u32) -> Self { + match val { + 0 => Self::OpenPGP, + 1 => Self::CMS, + 2 => Self::GPGCONF, + 3 => Self::ASSUAN, + 4 => Self::G13, + 5 => Self::UISERVER, + 6 => Self::SPAWN, + 254 => Self::DEFAULT, + _ => Self::UNKNOWN, + } + } } fn gpgme_error_try(lib: &libloading::Library, error_code: GpgmeError) -> Result<()> { @@ -1167,7 +1300,7 @@ enum DataKind { #[derive(Debug)] pub struct Data { - inner: std::ptr::NonNull, + inner: NonNull, kind: DataKind, #[allow(dead_code)] bytes: std::pin::Pin>, @@ -1200,14 +1333,26 @@ impl Drop for Data { struct GpgmeFd { fd: RawFd, fnc: GpgmeIOCb, - fnc_data: *mut ::std::os::raw::c_void, + fnc_data: *mut c_void, idx: usize, write: bool, - sender: smol::channel::Sender<()>, - receiver: smol::channel::Receiver<()>, + sender: Sender<()>, + receiver: Receiver<()>, io_state: Arc>, } +impl std::fmt::Debug for GpgmeFd { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.debug_struct(identify!(GpgmeFd)) + .field("fd", &self.fd) + .field("fnc", &self.fnc) + .field("fnc_data", &self.fnc_data) + .field("idx", &self.idx) + .field("write", &self.write) + .finish_non_exhaustive() + } +} + unsafe impl Send for GpgmeFd {} unsafe impl Sync for GpgmeFd {} @@ -1217,132 +1362,6 @@ impl AsRawFd for GpgmeFd { } } -#[derive(Clone)] -struct KeyInner { - inner: std::ptr::NonNull<_gpgme_key>, -} - -unsafe impl Send for KeyInner {} -unsafe impl Sync for KeyInner {} - -pub struct Key { - inner: KeyInner, - lib: Arc, -} -unsafe impl Send for Key {} -unsafe impl Sync for Key {} - -impl Clone for Key { - fn clone(&self) -> Self { - let lib = self.lib.clone(); - unsafe { - call!(&self.lib, gpgme_key_ref)(self.inner.inner.as_ptr()); - } - Self { - inner: self.inner.clone(), - lib, - } - } -} - -impl Key { - #[inline(always)] - fn new(inner: KeyInner, lib: Arc) -> Self { - Self { inner, lib } - } - - pub fn primary_uid(&self) -> Option
{ - unsafe { - if (*(self.inner.inner.as_ptr())).uids.is_null() { - return None; - } - let uid = (*(self.inner.inner.as_ptr())).uids; - if (*uid).name.is_null() && (*uid).email.is_null() { - None - } else if (*uid).name.is_null() { - Some(Address::new( - None, - CStr::from_ptr((*uid).email).to_string_lossy().to_string(), - )) - } else if (*uid).email.is_null() { - Some(Address::new( - None, - CStr::from_ptr((*uid).name).to_string_lossy().to_string(), - )) - } else { - Some(Address::new( - Some(CStr::from_ptr((*uid).name).to_string_lossy().to_string()), - CStr::from_ptr((*uid).email).to_string_lossy().to_string(), - )) - } - } - } - - pub fn revoked(&self) -> bool { - unsafe { (*self.inner.inner.as_ptr()).revoked() > 0 } - } - - pub fn expired(&self) -> bool { - unsafe { (*self.inner.inner.as_ptr()).expired() > 0 } - } - - pub fn disabled(&self) -> bool { - unsafe { (*self.inner.inner.as_ptr()).disabled() > 0 } - } - - pub fn invalid(&self) -> bool { - unsafe { (*self.inner.inner.as_ptr()).invalid() > 0 } - } - - pub fn can_encrypt(&self) -> bool { - unsafe { (*self.inner.inner.as_ptr()).can_encrypt() > 0 } - } - - pub fn can_sign(&self) -> bool { - unsafe { (*self.inner.inner.as_ptr()).can_sign() > 0 } - } - - pub fn secret(&self) -> bool { - unsafe { (*self.inner.inner.as_ptr()).secret() > 0 } - } - - pub fn fingerprint(&self) -> Cow<'_, str> { - (unsafe { CStr::from_ptr((*(self.inner.inner.as_ptr())).fpr) }).to_string_lossy() - } -} - -impl std::fmt::Debug for Key { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - fmt.debug_struct(crate::identify!(Key)) - .field("fingerprint", &self.fingerprint()) - .field("uid", &self.primary_uid()) - .field("can_encrypt", &self.can_encrypt()) - .field("can_sign", &self.can_sign()) - .field("secret", &self.secret()) - .field("revoked", &self.revoked()) - .field("expired", &self.expired()) - .field("invalid", &self.invalid()) - .finish() - } -} - -impl PartialEq for Key { - fn eq(&self, other: &Self) -> bool { - self.fingerprint() == other.fingerprint() - } -} - -impl Eq for Key {} - -impl Drop for Key { - #[inline] - fn drop(&mut self) { - unsafe { - call!(&self.lib, gpgme_key_unref)(self.inner.inner.as_ptr()); - } - } -} - //#[test] //fn test_gpgme() { // std::thread::spawn(move || { @@ -1368,3 +1387,75 @@ impl Drop for Key { // String::from_utf8_lossy(&plain.into_bytes().unwrap()) // ); //} + +mod wrapper { + use super::*; + + /// Wrapper type to free IO state and `add_priv`, `event_priv` leaked + /// references. + #[repr(transparent)] + pub(super) struct IoStateWrapper(ManuallyDrop>>); + + impl IoStateWrapper { + pub(super) fn new(state: IoState) -> (Arc, gpgme_io_cbs) { + let inner = Arc::new(Mutex::new(state)); + let add_priv = Arc::into_raw(Arc::clone(&inner)) + .cast_mut() + .cast::(); + let event_priv = Arc::into_raw(Arc::clone(&inner)) + .cast_mut() + .cast::(); + + let io_cbs = gpgme_io_cbs { + add: Some(io::gpgme_register_io_cb), + add_priv, + remove: Some(io::gpgme_remove_io_cb), + event: Some(io::gpgme_event_io_cb), + event_priv, + }; + + (Arc::new(Self(ManuallyDrop::new(inner))), io_cbs) + } + + pub(super) fn done_fut(&self) -> Result<(Done, Vec>)> { + let (done, fut) = if let Ok(io_state_lck) = self.0.lock() { + let done = io_state_lck.done.clone(); + ( + done, + io_state_lck + .ops + .values() + .map(|a| Async::new(a.clone()).unwrap()) + .collect::>>(), + ) + } else { + return Err(Error::new("Could not use gpgme library") + .set_details("The context's IO state mutex was poisoned.") + .set_kind(ErrorKind::Bug)); + }; + Ok((done, fut)) + } + } + + impl Drop for IoStateWrapper { + fn drop(&mut self) { + // SAFETY: struct unit value is ManuallyDrop, so no Drop impls are called on the + // uninit value. + // let inner = unsafe { ManuallyDrop::take(&mut self.0) }; + // SAFETY: take add_priv reference + unsafe { Arc::decrement_strong_count(&self.0) }; + // SAFETY: take event_priv reference + unsafe { Arc::decrement_strong_count(&self.0) }; + } + } + + impl std::ops::Deref for IoStateWrapper { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } +} + +use wrapper::IoStateWrapper; diff --git a/melib/src/gpgme/tests.rs b/melib/src/gpgme/tests.rs new file mode 100644 index 00000000..c4c0ec01 --- /dev/null +++ b/melib/src/gpgme/tests.rs @@ -0,0 +1,153 @@ +// +// meli +// +// Copyright 2024 Emmanouil Pitsidianakis +// +// This file is part of meli. +// +// meli is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// meli is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with meli. If not, see . +// +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later + +use std::{borrow::Cow, ffi::CString, future::Future}; + +use sealed_test::prelude::*; + +use crate::{ + gpgme::{Context, EngineInfo, Key, LocateKey, Protocol}, + Address, Result, +}; + +const PUBKEY: &[u8]=b"-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: GnuPG v2.1.0-gitb3c71eb (GNU/Linux)\r\n\r\nmQGiBDo41NoRBADSfQazKGYf8nokq6zUKH/6INtV6MypSzSGmX2XErnARkIIPPYj\r\ncQRQ8zCbGV7ZU2ezVbzhFLUSJveE8PZUzzCrLp1O2NSyBTRcR5HVSXW95nJfY8eV\r\npOvZRAKul0BVLh81kYTsrfzaaCjh9VWNP26LoeN2r+PjZyktXe7gM3C4SwCgoTxK\r\nWUVi9HoT2HCLY7p7oig5hEcEALdCJal0UYomX3nJapIVLVZg3vkidr1RICYMb2vz\r\n58i17h8sxEtobD1vdIKNejulntaRAXs4n0tDYD9z7pRlwG1CLz1R9WxYzeOOqUDr\r\nfnVXdmU8L/oVWABat8v1V7QQhjMMf+41fuzVwDMMGqjVPLhu4X6wp3A8uyM3YDnQ\r\nVMN1A/4n2G5gHoOvjqxn8Ch5tBAdMGfO8gH4RjQOwzm2R1wPQss/yzUN1+tlMZGX\r\nK2dQ2FCWC/hDUSNaEQRlI15wxxBNZ2RQwlzE2A8v113DpvyzOtv0QO95gJ1teCXC\r\n7j/BN9asgHaBBc39JLO/TcpuI7Hf8PQ5VcP2F0UE3lczGhXbLLRESm9lIFJhbmRv\r\nbSBIYWNrZXIgKHRlc3Qga2V5IHdpdGggcGFzc3BocmFzZSAiYWJjIikgPGpvZUBl\r\neGFtcGxlLmNvbT6IYgQTEQIAIgUCTbdXqQIbIwYLCQgHAwIGFQgCCQoLBBYCAwEC\r\nHgECF4AACgkQr4IkT5zZ/VUcCACfQvSPi//9/gBv8SVrK6O4DiyD+jAAn3LEnfF1\r\n4j6MjwlqXTqol2VgQn1yuQENBDo41N0QBACedJb7Qhm50JSPe1V+rSZKLHT5nc3l\r\n2k1n7//wNsJkgDW2J7snIRjGtSzeNxMPh+hVzFidzAf3sbOlARQoBrMPPKpnJWtm\r\n6LEDf2lSwO36l0/bo6qDRmiFRJoHWytTJEjxVwRclVt4bXqHfNw9FKhZZbcKeAN2\r\noHgmBVSU6edHdwADBQP+OGAkEG4PcfSb8x191R+wkV/q2hA5Ay9z289Dx2rO28CO\r\n4M2fhhcjSmgr6x0DsrkfESCiG47UGJ169eu+QqJwk3HiF4crGN9rE5+VelBVFtrd\r\nMWkX2rPLGQWyw8iCZKbeH8g/ujmkaLovSmalzDcLe4v1xSLaP7Fnfzit0iIGZAGI\r\nRgQYEQIABgUCOjjU3QAKCRCvgiRPnNn9VVSaAJ9+rj1lIQnRl20i8Rom2Hwbe3re\r\n9QCfSYFnkZUw0yKF2DfCfqrDzdGAsbaIRgQYEQIABgUCOjjU3gAKCRCvgiRPnNn9\r\nVe4iAJ9FrGMlFR7s+GWf1scTeeyrthKrPQCfSpc/Yps72aFI7hPfyIa9MuerVZ4=\r\n=QRit\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n"; + +const SECKEY: &[u8] = b"-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: GnuPG v2.1.0-gitb3c71eb (GNU/Linux)\r\n\r\nlQHpBDo41NoRBADSfQazKGYf8nokq6zUKH/6INtV6MypSzSGmX2XErnARkIIPPYj\r\ncQRQ8zCbGV7ZU2ezVbzhFLUSJveE8PZUzzCrLp1O2NSyBTRcR5HVSXW95nJfY8eV\r\npOvZRAKul0BVLh81kYTsrfzaaCjh9VWNP26LoeN2r+PjZyktXe7gM3C4SwCgoTxK\r\nWUVi9HoT2HCLY7p7oig5hEcEALdCJal0UYomX3nJapIVLVZg3vkidr1RICYMb2vz\r\n58i17h8sxEtobD1vdIKNejulntaRAXs4n0tDYD9z7pRlwG1CLz1R9WxYzeOOqUDr\r\nfnVXdmU8L/oVWABat8v1V7QQhjMMf+41fuzVwDMMGqjVPLhu4X6wp3A8uyM3YDnQ\r\nVMN1A/4n2G5gHoOvjqxn8Ch5tBAdMGfO8gH4RjQOwzm2R1wPQss/yzUN1+tlMZGX\r\nK2dQ2FCWC/hDUSNaEQRlI15wxxBNZ2RQwlzE2A8v113DpvyzOtv0QO95gJ1teCXC\r\n7j/BN9asgHaBBc39JLO/TcpuI7Hf8PQ5VcP2F0UE3lczGhXbLP4HAwL0A7A1a/jY\r\n6s5JxysLUpKA31U2SrKxePmkmzYSuAiValUVdfkmLRrLSwmNJSy5NcrBHGimja1O\r\nfUUmPTg465j1+vD/tERKb2UgUmFuZG9tIEhhY2tlciAodGVzdCBrZXkgd2l0aCBw\r\nYXNzcGhyYXNlICJhYmMiKSA8am9lQGV4YW1wbGUuY29tPohiBBMRAgAiBQJNt1ep\r\nAhsjBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCvgiRPnNn9VRwIAJ9C9I+L\r\n//3+AG/xJWsro7gOLIP6MACfcsSd8XXiPoyPCWpdOqiXZWBCfXKdAWAEOjjU3RAE\r\nAJ50lvtCGbnQlI97VX6tJkosdPmdzeXaTWfv//A2wmSANbYnuychGMa1LN43Ew+H\r\n6FXMWJ3MB/exs6UBFCgGsw88qmcla2bosQN/aVLA7fqXT9ujqoNGaIVEmgdbK1Mk\r\nSPFXBFyVW3hteod83D0UqFlltwp4A3ageCYFVJTp50d3AAMFA/44YCQQbg9x9Jvz\r\nHX3VH7CRX+raEDkDL3Pbz0PHas7bwI7gzZ+GFyNKaCvrHQOyuR8RIKIbjtQYnXr1\r\n675ConCTceIXhysY32sTn5V6UFUW2t0xaRfas8sZBbLDyIJkpt4fyD+6OaRoui9K\r\nZqXMNwt7i/XFIto/sWd/OK3SIgZkAf4HAwIoimqPHVJZM85dNw6JtvLKFvvmkm3X\r\nuoCUG5nU6cgk6vetUYiykuKpU4zG3mDtdZdIZf76hJJ6lZTSHH9frLy7bRYPfu/k\r\nU1AFd1T1OxENiEYEGBECAAYFAjo41N0ACgkQr4IkT5zZ/VVUmgCffq49ZSEJ0Zdt\r\nIvEaJth8G3t63vUAn0mBZ5GVMNMihdg3wn6qw83RgLG2iEYEGBECAAYFAjo41N4A\r\nCgkQr4IkT5zZ/VXuIgCfRaxjJRUe7Phln9bHE3nsq7YSqz0An0qXP2KbO9mhSO4T\r\n38iGvTLnq1We\r\n=m0YJ\r\n-----END PGP PRIVATE KEY BLOCK-----\r\n"; + +#[sealed_test] +fn test_gpgme_verify_sig() { + fn make_fut( + secret: bool, + local: bool, + pattern: String, + ctx: &mut Context, + ) -> Result>>> { + if local { + ctx.set_auto_key_locate(LocateKey::LOCAL)?; + } else { + ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?; + } + ctx.keylist(secret, Some(pattern)) + } + + let tempdir = tempfile::tempdir().unwrap(); + + unsafe { + std::env::set_var("GNUPGHOME", tempdir.path()); + } + unsafe { + std::env::set_var("GPG_AGENT_INFO", ""); + } + + let mut gpgme_ctx = match Context::new() { + Ok(v) => v, + Err(err) if err.kind.is_not_found() => { + eprintln!("INFO: libgpgme could not be loaded, skipping this test."); + return; + } + err => err.unwrap(), + }; + gpgme_ctx.set_protocol(Protocol::OpenPGP).unwrap(); + let current_engine_info = gpgme_ctx.engine_info().unwrap(); + let prev_len = current_engine_info.len(); + let Some(EngineInfo { + file_name: Some(engine_file_name), + .. + }) = current_engine_info + .into_iter() + .find(|eng| eng.protocol == Protocol::OpenPGP) + else { + eprintln!("WARN: No openpg protocol engine returned from gpgme."); + return; + }; + gpgme_ctx + .set_engine_info( + Protocol::OpenPGP, + Some(Cow::Owned(CString::new(engine_file_name).unwrap())), + Some(Cow::Owned( + CString::new(tempdir.path().display().to_string()).unwrap(), + )), + ) + .unwrap(); + let new_engine_info = gpgme_ctx.engine_info().unwrap(); + assert_eq!( + new_engine_info.len(), + prev_len, + "new_engine_info was expected to have {} entry/ies but has {}: {:#?}", + prev_len, + new_engine_info.len(), + new_engine_info + ); + assert_eq!( + new_engine_info[0].home_dir, + Some(tempdir.path().display().to_string()), + "new_engine_info was expected to have temp dir as home_dir but has: {:#?}", + new_engine_info[0].home_dir + ); + let mut pubkey_data = Some(gpgme_ctx.new_data_mem(PUBKEY).unwrap()); + for _ in 0..2 { + let keylist: Vec = smol::block_on(async { + make_fut(false, true, "".to_string(), &mut gpgme_ctx) + .unwrap() + .await + }) + .unwrap(); + + if let Some(pubkey_data) = pubkey_data.take() { + assert_eq!( + &keylist, + &[], + "keylist should have been empty but is: {:?}", + keylist + ); + gpgme_ctx.import_key(pubkey_data).unwrap(); + } else { + let assert_key = |key: &Key| { + key.fingerprint() == "ADAB7FCC1F4DE2616ECFA402AF82244F9CD9FD55" + && key.primary_uid() + == Some(Address::new( + Some("Joe Random Hacker".into()), + "joe@example.com".into(), + )) + && key.can_encrypt() + && key.can_sign() + && !key.secret() + && !key.revoked() + && !key.expired() + && !key.invalid() + }; + assert_eq!(keylist.len(), 1); + assert!( + assert_key(&keylist[0]), + "keylist expected to have {:?} but got {:?}", + "ADAB7FCC1F4DE2616ECFA402AF82244F9CD9FD55", + keylist[0] + ); + } + } + gpgme_ctx + .import_key(gpgme_ctx.new_data_mem(SECKEY).unwrap()) + .unwrap(); + gpgme_ctx + .import_key(gpgme_ctx.new_data_mem(SECKEY).unwrap()) + .unwrap_err(); +}