summaryrefslogtreecommitdiffstats
path: root/ipc/src
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2019-06-08 14:47:27 +0200
committerJustus Winter <justus@sequoia-pgp.org>2019-06-26 09:20:33 +0200
commit433437b20b8cb33263a293f34589e29eeacabfca (patch)
tree9b2d2ec214f8595f6e8c7e4a741ea359ba89f586 /ipc/src
parentadcf04a7a2489ff4d5fe983fe7caa76c22fded2b (diff)
ipc: GnuPG RPC support.
- This allows us to communicate with gpg-agent, and use it for cryptographic operations.
Diffstat (limited to 'ipc/src')
-rw-r--r--ipc/src/gnupg.rs764
-rw-r--r--ipc/src/lib.rs1
2 files changed, 765 insertions, 0 deletions
diff --git a/ipc/src/gnupg.rs b/ipc/src/gnupg.rs
new file mode 100644
index 00000000..4236c9c4
--- /dev/null
+++ b/ipc/src/gnupg.rs
@@ -0,0 +1,764 @@
+//! GnuPG RPC support.
+
+#![warn(missing_docs)]
+
+use std::collections::BTreeMap;
+use std::ops::{Deref, DerefMut};
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use futures::{Async, Future, Stream};
+
+extern crate libc;
+extern crate tempfile;
+
+use openpgp::constants::HashAlgorithm;
+use openpgp::conversions::hex;
+use openpgp::crypto;
+use openpgp::crypto::sexp::Sexp;
+use openpgp::parse::Parse;
+use openpgp::serialize::Serialize;
+
+use Result;
+use assuan;
+
+/// A GnuPG context.
+#[derive(Debug)]
+pub struct Context {
+ homedir: Option<PathBuf>,
+ components: BTreeMap<String, PathBuf>,
+ directories: BTreeMap<String, PathBuf>,
+ sockets: BTreeMap<String, PathBuf>,
+ #[allow(dead_code)] // We keep it around for the cleanup.
+ ephemeral: Option<tempfile::TempDir>,
+}
+
+impl Context {
+ /// Creates a new context for the default GnuPG home directory.
+ pub fn new() -> Result<Self> {
+ Self::make(None, None)
+ }
+
+ /// Creates a new context for the given GnuPG home directory.
+ pub fn with_homedir<P>(homedir: P) -> Result<Self>
+ where P: AsRef<Path>
+ {
+ Self::make(Some(homedir.as_ref()), None)
+ }
+
+ /// Creates a new ephemeral context.
+ ///
+ /// The created home directory will be deleted once this object is
+ /// dropped.
+ pub fn ephemeral() -> Result<Self> {
+ Self::make(None, Some(tempfile::tempdir()?))
+ }
+
+ fn make(homedir: Option<&Path>, ephemeral: Option<tempfile::TempDir>)
+ -> Result<Self> {
+ let mut components: BTreeMap<String, PathBuf> = Default::default();
+ let mut directories: BTreeMap<String, PathBuf> = Default::default();
+ let mut sockets: BTreeMap<String, PathBuf> = Default::default();
+
+ let homedir: Option<PathBuf> =
+ ephemeral.as_ref().map(|tmp| tmp.path()).or(homedir)
+ .map(|p| p.into());
+
+ for fields in Self::gpgconf(
+ &homedir, &["--list-components"], 3)?.into_iter()
+ {
+ components.insert(String::from_utf8(fields[0].clone())?,
+ String::from_utf8(fields[2].clone())?.into());
+ }
+
+ for fields in Self::gpgconf(&homedir, &["--list-dirs"], 2)?.into_iter()
+ {
+ let (mut key, value) = (fields[0].clone(), fields[1].clone());
+ if key.ends_with(b"-socket") {
+ let l = key.len();
+ key.truncate(l - b"-socket".len());
+ sockets.insert(String::from_utf8(key)?,
+ String::from_utf8(value)?.into());
+ } else {
+ directories.insert(String::from_utf8(key)?,
+ String::from_utf8(value)?.into());
+ }
+ }
+
+ Ok(Context {
+ homedir,
+ components,
+ directories,
+ sockets,
+ ephemeral,
+ })
+ }
+
+ fn gpgconf(homedir: &Option<PathBuf>, arguments: &[&str], nfields: usize)
+ -> Result<Vec<Vec<Vec<u8>>>> {
+ let nl = |&c: &u8| c as char == '\n';
+ let colon = |&c: &u8| c as char == ':';
+
+ let mut gpgconf = Command::new("gpgconf");
+ if let Some(homedir) = homedir {
+ gpgconf.arg("--homedir").arg(homedir);
+
+ // https://dev.gnupg.org/T4496
+ gpgconf.env("GNUPGHOME", homedir);
+ }
+
+ for argument in arguments {
+ gpgconf.arg(argument);
+ }
+ let output = gpgconf.output().map_err(|e| -> failure::Error {
+ Error::GPGConf(e.to_string()).into()
+ })?;
+
+ if output.status.success() {
+ let mut result = Vec::new();
+ for line in output.stdout.split(nl) {
+ if line.len() == 0 {
+ // EOF.
+ break;
+ }
+
+ let fields =
+ line.splitn(nfields, colon).map(|f| f.to_vec())
+ .collect::<Vec<_>>();
+
+ if fields.len() != nfields {
+ return Err(Error::GPGConf(
+ format!("Malformed response, expected {} fields, \
+ on line: {:?}", nfields, line)).into());
+ }
+
+ result.push(fields);
+ }
+ Ok(result)
+ } else {
+ Err(Error::GPGConf(String::from_utf8_lossy(
+ &output.stderr).into_owned()).into())
+ }
+ }
+
+ /// Returns the path to a GnuPG component.
+ pub fn component<C>(&self, component: C) -> Result<&Path>
+ where C: AsRef<str>
+ {
+ self.components.get(component.as_ref())
+ .map(|p| p.as_path())
+ .ok_or_else(|| {
+ Error::GPGConf(format!("No such component {:?}",
+ component.as_ref())).into()
+ })
+ }
+
+ /// Returns the path to a GnuPG directory.
+ pub fn directory<C>(&self, directory: C) -> Result<&Path>
+ where C: AsRef<str>
+ {
+ self.directories.get(directory.as_ref())
+ .map(|p| p.as_path())
+ .ok_or_else(|| {
+ Error::GPGConf(format!("No such directory {:?}",
+ directory.as_ref())).into()
+ })
+ }
+
+ /// Returns the path to a GnuPG socket.
+ pub fn socket<C>(&self, socket: C) -> Result<&Path>
+ where C: AsRef<str>
+ {
+ self.sockets.get(socket.as_ref())
+ .map(|p| p.as_path())
+ .ok_or_else(|| {
+ Error::GPGConf(format!("No such socket {:?}",
+ socket.as_ref())).into()
+ })
+ }
+
+ /// Creates directories for RPC communication.
+ pub fn create_socket_dir(&self) -> Result<()> {
+ Self::gpgconf(&self.homedir, &["--create-socketdir"], 1)?;
+ Ok(())
+ }
+
+ /// Removes directories for RPC communication.
+ ///
+ /// Note: This will stop all servers once they note that their
+ /// socket is gone.
+ pub fn remove_socket_dir(&self) -> Result<()> {
+ Self::gpgconf(&self.homedir, &["--remove-socketdir"], 1)?;
+ Ok(())
+ }
+
+ /// Starts a GnuPG component.
+ pub fn start(&self, component: &str) -> Result<()> {
+ self.create_socket_dir()?;
+ Self::gpgconf(&self.homedir, &["--launch", component], 1)?;
+ Ok(())
+ }
+
+ /// Stops a GnuPG component.
+ pub fn stop(&self, component: &str) -> Result<()> {
+ Self::gpgconf(&self.homedir, &["--kill", component], 1)?;
+ Ok(())
+ }
+
+ /// Stops all GnuPG components.
+ pub fn stop_all(&self) -> Result<()> {
+ self.stop("all")
+ }
+}
+
+impl Drop for Context {
+ fn drop(&mut self) {
+ if self.ephemeral.is_some() {
+ let _ = self.stop_all();
+ let _ = self.remove_socket_dir();
+ }
+ }
+}
+
+/// A connection to a GnuPG agent.
+pub struct Agent {
+ c: assuan::Client,
+}
+
+impl Deref for Agent {
+ type Target = assuan::Client;
+
+ fn deref(&self) -> &Self::Target {
+ &self.c
+ }
+}
+
+impl DerefMut for Agent {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.c
+ }
+}
+
+impl Stream for Agent {
+ type Item = assuan::Response;
+ type Error = failure::Error;
+
+ /// Attempt to pull out the next value of this stream, returning
+ /// None if the stream is finished.
+ ///
+ /// Note: It _is_ safe to call this again after the stream
+ /// finished, i.e. returned `Ready(None)`.
+ fn poll(&mut self)
+ -> std::result::Result<Async<Option<Self::Item>>, Self::Error>
+ {
+ self.c.poll()
+ }
+}
+
+impl Agent {
+ /// Connects to the agent.
+ ///
+ /// Note: This function does not try to start the server. If no
+ /// server is running for the given context, this operation will
+ /// fail.
+ pub fn connect<'c>(ctx: &'c Context)
+ -> impl Future<Item = Self, Error = failure::Error> + 'c
+ {
+ futures::lazy(move || ctx.socket("agent"))
+ .and_then(Self::connect_to)
+ }
+
+ /// Connects to the agent at the given path.
+ ///
+ /// Note: This function does not try to start the server. If no
+ /// server is running for the given context, this operation will
+ /// fail.
+ pub fn connect_to<P>(path: P)
+ -> impl Future<Item = Self, Error = failure::Error>
+ where P: AsRef<Path>
+ {
+ assuan::Client::connect(path)
+ .and_then(|c| Ok(Agent { c }))
+ }
+
+ /// Creates a signature over the `digest` produced by `algo` using
+ /// `key` with the secret bits managed by the agent.
+ pub fn sign<'a>(&'a mut self, key: &'a openpgp::packet::Key,
+ algo: HashAlgorithm, digest: &'a [u8])
+ -> impl Future<Item = crypto::mpis::Signature,
+ Error = failure::Error> + 'a
+ {
+ SigningRequest::new(&mut self.c, key, algo, digest)
+ }
+
+ /// Decrypts `ciphertext` using `key` with the secret bits managed
+ /// by the agent.
+ pub fn decrypt<'a>(&'a mut self, key: &'a openpgp::packet::Key,
+ ciphertext: &'a crypto::mpis::Ciphertext)
+ -> impl Future<Item = crypto::SessionKey,
+ Error = failure::Error> + 'a
+ {
+ DecryptionRequest::new(&mut self.c, key, ciphertext)
+ }
+
+ /// Computes options that we want to communicate.
+ fn options() -> Vec<String> {
+ use std::env::var;
+ use std::ffi::CStr;
+
+ let mut r = Vec::new();
+
+ if let Ok(tty) = var("GPG_TTY") {
+ r.push(format!("OPTION ttyname={}", tty));
+ } else {
+ unsafe {
+ let tty = libc::ttyname(0);
+ if ! tty.is_null() {
+ if let Ok(tty) = CStr::from_ptr(tty).to_str() {
+ r.push(format!("OPTION ttyname={}", tty));
+ }
+ }
+ }
+ }
+
+ if let Ok(term) = var("TERM") {
+ r.push(format!("OPTION ttytype={}", term));
+ }
+
+ if let Ok(display) = var("DISPLAY") {
+ r.push(format!("OPTION display={}", display));
+ }
+
+ if let Ok(xauthority) = var("XAUTHORITY") {
+ r.push(format!("OPTION xauthority={}", xauthority));
+ }
+
+ if let Ok(dbus) = var("DBUS_SESSION_BUS_ADDRESS") {
+ r.push(format!("OPTION putenv=DBUS_SESSION_BUS_ADDRESS={}", dbus));
+ }
+
+ // We're going to pop() options off the end, therefore reverse
+ // the vec here to preserve the above ordering, which is the
+ // one GnuPG uses.
+ r.reverse();
+ r
+ }
+}
+
+struct SigningRequest<'a, 'b, 'c> {
+ c: &'a mut assuan::Client,
+ key: &'b openpgp::packet::Key,
+ algo: HashAlgorithm,
+ digest: &'c [u8],
+ options: Vec<String>,
+ state: SigningRequestState,
+}
+
+impl<'a, 'b, 'c> SigningRequest<'a, 'b, 'c> {
+ fn new(c: &'a mut assuan::Client,
+ key: &'b openpgp::packet::Key,
+ algo: HashAlgorithm,
+ digest: &'c [u8])
+ -> Self {
+ Self {
+ c, key, algo, digest,
+ options: Agent::options(),
+ state: SigningRequestState::Start,
+ }
+ }
+}
+
+#[derive(Debug)]
+enum SigningRequestState {
+ Start,
+ Options,
+ SigKey,
+ SetHash,
+ PkSign(Vec<u8>),
+}
+
+/// Returns a convenient Err value for use in the state machines
+/// below.
+fn operation_failed<T>(message: &Option<String>) -> Result<T> {
+ Err(Error::OperationFailed(
+ message.as_ref().map(|e| e.to_string())
+ .unwrap_or_else(|| "Unknown reason".into()))
+ .into())
+}
+
+/// Returns a convenient Err value for use in the state machines
+/// below.
+fn protocol_error<T>(response: &assuan::Response) -> Result<T> {
+ Err(Error::ProtocolError(
+ format!("Got unexpected response {:?}", response))
+ .into())
+}
+
+impl<'a, 'b, 'c> Future for SigningRequest<'a, 'b, 'c> {
+ type Item = crypto::mpis::Signature;
+ type Error = failure::Error;
+
+ fn poll(&mut self) -> std::result::Result<Async<Self::Item>, Self::Error> {
+ use self::SigningRequestState::*;
+
+ loop {
+ match self.state {
+ Start => {
+ if self.options.is_empty() {
+ self.c.send(format!("SIGKEY {}",
+ self.key.mpis().keygrip()?))?;
+ self.state = SigKey;
+ } else {
+ self.c.send(self.options.pop().unwrap())?;
+ self.state = Options;
+ }
+ },
+
+ Options => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+ assuan::Response::Ok { .. }
+ | assuan::Response::Comment { .. }
+ | assuan::Response::Status { .. } =>
+ (), // Ignore.
+ assuan::Response::Error { ref message, .. } =>
+ return operation_failed(message),
+ _ =>
+ return protocol_error(&r),
+ },
+ Async::Ready(None) => {
+ if let Some(option) = self.options.pop() {
+ self.c.send(option)?;
+ } else {
+ self.c.send(format!("SIGKEY {}",
+ self.key.mpis().keygrip()?))?;
+ self.state = SigKey;
+ }
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+
+ SigKey => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+ assuan::Response::Ok { .. }
+ | assuan::Response::Comment { .. }
+ | assuan::Response::Status { .. } =>
+ (), // Ignore.
+ assuan::Response::Error { ref message, .. } =>
+ return operation_failed(message),
+ _ =>
+ return protocol_error(&r),
+ },
+ Async::Ready(None) => {
+ self.c.send(format!("SETHASH {} {}",
+ u8::from(self.algo),
+ hex::encode(&self.digest)))?;
+ self.state = SetHash;
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+
+ SetHash => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+ assuan::Response::Ok { .. }
+ | assuan::Response::Comment { .. }
+ | assuan::Response::Status { .. } =>
+ (), // Ignore.
+ assuan::Response::Error { ref message, .. } =>
+ return operation_failed(message),
+ _ =>
+ return protocol_error(&r),
+ },
+ Async::Ready(None) => {
+ self.c.send("PKSIGN")?;
+ self.state = PkSign(Vec::new());
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+
+
+ PkSign(ref mut data) => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+ assuan::Response::Ok { .. }
+ | assuan::Response::Comment { .. }
+ | assuan::Response::Status { .. } =>
+ (), // Ignore.
+ assuan::Response::Error { ref message, .. } =>
+ return operation_failed(message),
+ assuan::Response::Data { ref partial } =>
+ data.extend_from_slice(partial),
+ _ =>
+ return protocol_error(&r),
+ },
+ Async::Ready(None) => {
+ return Ok(Async::Ready(
+ Sexp::from_bytes(&data)?.to_signature()?));
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+ }
+ }
+ }
+}
+
+struct DecryptionRequest<'a, 'b, 'c> {
+ c: &'a mut assuan::Client,
+ key: &'b openpgp::packet::Key,
+ ciphertext: &'c crypto::mpis::Ciphertext,
+ options: Vec<String>,
+ state: DecryptionRequestState,
+}
+
+impl<'a, 'b, 'c> DecryptionRequest<'a, 'b, 'c> {
+ fn new(c: &'a mut assuan::Client,
+ key: &'b openpgp::packet::Key,
+ ciphertext: &'c crypto::mpis::Ciphertext)
+ -> Self {
+ Self {
+ c,
+ key,
+ ciphertext,
+ options: Agent::options(),
+ state: DecryptionRequestState::Start,
+ }
+ }
+}
+
+#[derive(Debug)]
+enum DecryptionRequestState {
+ Start,
+ Options,
+ SetKey,
+ PkDecrypt,
+ Inquire(Vec<u8>, bool), // Buffer and padding.
+}
+
+impl<'a, 'b, 'c> Future for DecryptionRequest<'a, 'b, 'c> {
+ type Item = crypto::SessionKey;
+ type Error = failure::Error;
+
+ fn poll(&mut self) -> std::result::Result<Async<Self::Item>, Self::Error> {
+ use self::DecryptionRequestState::*;
+
+ loop {
+ match self.state {
+ Start => {
+ if self.options.is_empty() {
+ self.c.send(format!("SETKEY {}",
+ self.key.mpis().keygrip()?))?;
+ self.state = SetKey;
+ } else {
+ self.c.send(self.options.pop().unwrap())?;
+ self.state = Options;
+ }
+ },
+
+ Options => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+ assuan::Response::Ok { .. }
+ | assuan::Response::Comment { .. }
+ | assuan::Response::Status { .. } =>
+ (), // Ignore.
+ assuan::Response::Error { ref message, .. } =>
+ return operation_failed(message),
+ _ =>
+ return protocol_error(&r),
+ },
+ Async::Ready(None) => {
+ if let Some(option) = self.options.pop() {
+ self.c.send(option)?;
+ } else {
+ self.c.send(format!("SETKEY {}",
+ self.key.mpis().keygrip()?))?;
+ self.state = SetKey;
+ }
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+
+ SetKey => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+ assuan::Response::Ok { .. }
+ | assuan::Response::Comment { .. }
+ | assuan::Response::Status { .. } =>
+ (), // Ignore.
+ assuan::Response::Error { ref message, .. } =>
+ return operation_failed(message),
+ _ =>
+ return protocol_error(&r),
+ },
+ Async::Ready(None) => {
+ self.c.send("PKDECRYPT")?;
+ self.state = PkDecrypt;
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+
+ PkDecrypt => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+ assuan::Response::Inquire { ref keyword, .. }
+ if keyword == "CIPHERTEXT" =>
+ (), // What we expect.
+ assuan::Response::Comment { .. }
+ | assuan::Response::Status { .. } =>
+ (), // Ignore.
+ assuan::Response::Error { ref message, .. } =>
+ return operation_failed(message),
+ _ =>
+ return protocol_error(&r),
+ },
+ Async::Ready(None) => {
+ let mut buf = Vec::new();
+ Sexp::from_ciphertext(&self.ciphertext)?
+ .serialize(&mut buf)?;
+ self.c.data(&buf)?;
+ self.state = Inquire(Vec::new(), true);
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+
+
+ Inquire(ref mut data, ref mut padding) => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+ assuan::Response::Ok { .. }
+ | assuan::Response::Comment { .. } =>
+ (), // Ignore.
+ assuan::Response::Status { ref keyword, ref message } =>
+ if keyword == "PADDING" {
+ *padding = &message != &"0";
+ },
+ assuan::Response::Error { ref message, .. } =>
+ return operation_failed(message),
+ assuan::Response::Data { ref partial } =>
+ data.extend_from_slice(partial),
+ _ =>
+ return protocol_error(&r),
+ },
+ Async::Ready(None) => {
+ // Get rid of the safety-0.
+ //
+ // gpg-agent seems to add a trailing 0,
+ // supposedly for good measure.
+ if data.iter().last() == Some(&0) {
+ let l = data.len();
+ data.truncate(l - 1);
+ }
+
+ return Ok(Async::Ready(
+ Sexp::from_bytes(&data)?.finish_decryption(
+ self.key, self.ciphertext, *padding)?
+ ));
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+ }
+ }
+ }
+}
+
+/// A cryptographic key pair.
+///
+/// A `KeyPair` is a combination of public and secret key. This
+/// particular implementation does not have the secret key, but
+/// diverges the cryptographic operations to `gpg-agent`.
+pub struct KeyPair<'a> {
+ public: &'a openpgp::packet::Key,
+ agent_socket: PathBuf,
+}
+
+impl<'a> KeyPair<'a> {
+ /// Returns a `KeyPair` for `key` with the secret bits managed by
+ /// the agent.
+ ///
+ /// This provides a convenient, synchronous interface for use with
+ /// the low-level Sequoia crate.
+ pub fn new(ctx: &Context, key: &'a openpgp::packet::Key)
+ -> Result<KeyPair<'a>> {
+ Ok(KeyPair {
+ public: key,
+ agent_socket: ctx.socket("agent")?.into(),
+ })
+ }
+}
+
+impl<'a> crypto::Signer for KeyPair<'a> {
+ fn public(&self) -> &openpgp::packet::Key {
+ self.public
+ }
+
+ fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8])
+ -> Result<openpgp::crypto::mpis::Signature>
+ {
+ use openpgp::constants::PublicKeyAlgorithm::*;
+ use openpgp::crypto::mpis::PublicKey;
+
+ #[allow(deprecated)]
+ match (self.public.pk_algo(), self.public.mpis())
+ {
+ (RSASign, PublicKey::RSA { .. })
+ | (RSAEncryptSign, PublicKey::RSA { .. })
+ | (DSA, PublicKey::DSA { .. })
+ | (EdDSA, PublicKey::EdDSA { .. })
+ | (ECDSA, PublicKey::ECDSA { .. }) => {
+ let mut a = Agent::connect_to(&self.agent_socket).wait()?;
+ let sig = a.sign(self.public, hash_algo, digest).wait()?;
+ Ok(sig)
+ },
+
+ (pk_algo, _) => Err(openpgp::Error::InvalidOperation(format!(
+ "unsupported combination of algorithm {:?} and key {:?}",
+ pk_algo, self.public)).into()),
+ }
+ }
+}
+
+impl<'a> crypto::Decryptor for KeyPair<'a> {
+ fn public(&self) -> &openpgp::packet::Key {
+ self.public
+ }
+
+ fn decrypt(&mut self, ciphertext: &crypto::mpis::Ciphertext)
+ -> Result<crypto::SessionKey>
+ {
+ use openpgp::crypto::mpis::{PublicKey, Ciphertext};
+
+ match (self.public.mpis(), ciphertext) {
+ (PublicKey::RSA { .. }, Ciphertext::RSA { .. })
+ | (PublicKey::Elgamal { .. }, Ciphertext::Elgamal { .. })
+ | (PublicKey::ECDH { .. }, Ciphertext::ECDH { .. }) => {
+ let mut a = Agent::connect_to(&self.agent_socket).wait()?;
+ let sk = a.decrypt(self.public, ciphertext).wait()?;
+ Ok(sk)
+ },
+
+ (public, ciphertext) =>
+ Err(openpgp::Error::InvalidOperation(format!(
+ "unsupported combination of key pair {:?} \
+ and ciphertext {:?}",
+ public, ciphertext)).into()),
+ }
+ }
+}
+
+
+#[derive(Fail, Debug)]
+/// Errors used in this module.
+pub enum Error {
+ /// Errors related to `gpgconf`.
+ #[fail(display = "gpgconf: {}", _0)]
+ GPGConf(String),
+ /// The remote operation failed.
+ #[fail(display = "Operation failed: {}", _0)]
+ OperationFailed(String),
+ /// The remote party violated the protocol.
+ #[fail(display = "Protocol violation: {}", _0)]
+ ProtocolError(String),
+
+}
diff --git a/ipc/src/lib.rs b/ipc/src/lib.rs
index eee2ba69..25d28ef1 100644
--- a/ipc/src/lib.rs
+++ b/ipc/src/lib.rs
@@ -80,6 +80,7 @@ use sequoia_core as core;
#[macro_use] mod trace;
pub mod assuan;
+pub mod gnupg;
/// Servers need to implement this trait.
pub trait Handler {