//! This crate includes functionality for interacting with GnuPG's
//! `gpg-agent`.
//!
//! `gpg-agent` is a secret key store, which is shipped as part of GnuPG.
//! It is used to manage secret key material, and hardware devices that
//! contain secret key material.  It provides an RPC interface using the
//! [Assuan protocol].
//!
//!   [Assuan protocol]: https://gnupg.org/software/libassuan
//!
//! This is how `gpg`, GnuPG's primary command-line interface,
//! communicates with it.
//!
//! This crate provides a Rust API for interacting with `gpg-agent`.
//!
//! Note: this crate communicates directly with `gpg-agent`; it does not
//! go via `gpg`.
//!
//! # Examples
//!
//! Import secret key material, list the keys, and sign a message:
//!
//! ```rust
//! use std::io::{Read, Write};
//!
//! use sequoia_openpgp as openpgp;
//! use openpgp::Cert;
//! # use openpgp::crypto::hash::Digest;
//! use openpgp::packet::signature;
//! use openpgp::parse::Parse;
//! use openpgp::policy::StandardPolicy;
//! use openpgp::serialize::stream::{Message, Signer, LiteralWriter};
//! use openpgp::types::HashAlgorithm;
//! use openpgp::types::SignatureType;
//!
//! use sequoia_gpg_agent as gpg_agent;
//! use gpg_agent::KeyPair;
//!
//! const P: &StandardPolicy = &StandardPolicy::new();
//!
//! # let _: Result<(), anyhow::Error> = tokio_test::block_on(async {
//! #
//! // Load a TSK, i.e., a certificate with secret key material.
//! let key = // ...
//! # "\
//! # -----BEGIN PGP PRIVATE KEY BLOCK-----
//! # Comment: B6E6 9975 5F27 FF3D 002D  BD26 0390 037C 6D2B DA9A
//! #
//! # xVgEZewcTxYJKwYBBAHaRw8BAQdAitNrSMaEq1ZCXBtefsgJfGHdipTrZnTKXf6R
//! # mnueg9oAAQC8xOJk0vjPPQNEkXc5xlm7w+f2C7EGQ0/GQYuxTVZAoRC6wsALBB8W
//! # CgB9BYJl7BxPAwsJBwkQA5ADfG0r2ppHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu
//! # c2VxdW9pYS1wZ3Aub3JnNHQtwZonz3Z3xLL1M7jK/Sl4R1TNDqZ+xeBhXSzbOAkD
//! # FQoIApsBAh4BFiEEtuaZdV8n/z0ALb0mA5ADfG0r2poAAKCuAP4ncgpWq32cIDQU
//! # ApwklDTPpWpahIuYsL0LdIjkOnVNrQD/XXxnSIVPpD9ax6Gkwdjrq21rUSnQOQcb
//! # LPJ9jbSS2wHHWARl7BxPFgkrBgEEAdpHDwEBB0Brn/OSMDjcjVvcUrY+wYjtpQDw
//! # BFpow6NrHs4X5K2wkgAA/3sqHgNHP16HDEwfBteL4YfdXIj0fDBHmvLy6csYieke
//! # D/nCwL8EGBYKATEFgmXsHE8JEAOQA3xtK9qaRxQAAAAAAB4AIHNhbHRAbm90YXRp
//! # b25zLnNlcXVvaWEtcGdwLm9yZyUyW5yXDcLc93rxkYdz7tnHpOKjviJn17/hCQw/
//! # p3YkApsCvqAEGRYKAG8FgmXsHE8JEAiqGShDW50RRxQAAAAAAB4AIHNhbHRAbm90
//! # YXRpb25zLnNlcXVvaWEtcGdwLm9yZx0ribmnTiQaPn4ftkFsyOYwo4cerHsFSjch
//! # sIRTjGosFiEEHXtwtDIzaa3fXh2iCKoZKENbnREAAH6OAP9aIMae+BJWPfxlf1cL
//! # 4wwqTihcctO8LjaUk/TZVc8I6AEAlCMAn9SinU0FhgGr8RThuY2Nj8dwzEdCbhVu
//! # l9jxrQ0WIQS25pl1Xyf/PQAtvSYDkAN8bSvamgAA1Q0A/A4aKmIY7aNShNoJ5cT3
//! # SmqCrmFBQuIAXY5QhKu44FtcAQDZ4nB8GNORlAe7q719lLV+jo/BymIRstcU0R3R
//! # lfqgCg==
//! # =B79/
//! # -----END PGP PRIVATE KEY BLOCK-----
//! # ";
//! let cert = Cert::from_bytes(key)?;
//! assert!(cert.is_tsk());
//!
//! // Connect to a temporary GnuPG home directory.  Usually,
//! // you'll want to connect to the default home directory using
//! // Agent::connect_to_default.
//! let mut agent = gpg_agent::Agent::connect_to_ephemeral().await?;
//!
//! // Import the secret key material into gpg-agent.
//! for k in cert.keys().secret() {
//!     agent.import(P,
//!                  &cert, k.parts_as_secret().expect("have secret"),
//!                  true, true).await?;
//! }
//!
//! // List the keys.
//! let list = agent.list_keys().await?;
//! # assert_eq!(list.len(), cert.keys().secret().count());
//! println!("gpg agent manages {} keys:", list.len());
//! for k in list.iter() {
//!     eprintln!("  - {}", k.keygrip());
//! }
//!
//! // Sign a message using the signing key we just imported.
//! let signing_key = cert.with_policy(P, None)?
//!     .keys().for_signing()
//!     .next().expect("Have a signing-capable subkey.");
//!
//! let mut keypair = agent.keypair(&signing_key)?;
//!
//! let mut signed_message = Vec::new();
//! let message = Message::new(&mut signed_message);
//! let message = Signer::new(message, keypair).build()?;
//! let mut message = LiteralWriter::new(message).build()?;
//! message.write_all(b"I love you!")?;
//! message.finalize()?;
//!
//! # Ok(()) });
//! ```
use std::{
    convert::TryFrom,
    path::Path,
    path::PathBuf,
    ops::Deref,
    ops::DerefMut,
    pin::Pin,
};

use sequoia_openpgp as openpgp;
use sequoia_ipc as ipc;
use openpgp::{
    Cert,
    cert::ValidCert,
    crypto::{
        self,
        Password,
        S2K,
        mem::Protected,
        mpi::SecretKeyChecksum,
    },
    fmt::hex,
    packet::{
        Key,
        key::{
            KeyRole,
            PublicParts,
            SecretParts,
            UnspecifiedRole,
            SecretKeyMaterial,
        },
        SKESK,
    },
    parse::Parse,
    policy::Policy,
    types::{
        HashAlgorithm,
        Timestamp,
    },
};
use ipc::{
    Keygrip,
    sexp::Sexp,
};

use futures::stream::StreamExt;

#[macro_use]
mod macros;

mod babel;
mod utils;

pub mod assuan;
use assuan::Response;
use assuan::escape;

pub mod gnupg;
pub use gnupg::Context;

pub mod keyinfo;
pub mod cardinfo;

#[cfg(test)]
mod tests;

trace_module!(TRACE);

/// Reexport sequoia_ipc.
///
/// Some of our public types use types defined in this crate.
pub use sequoia_ipc;

#[derive(thiserror::Error, Debug)]
/// Errors used in this module.
#[non_exhaustive]
pub enum Error {
    /// GnuPG's home directory doesn't exist.
    #[error("GnuPG's home directory ({0}) doesn't exist")]
    GnuPGHomeMissing(PathBuf),
    /// Unknown key.
    #[error("Unknown key: {0}")]
    UnknownKey(Keygrip),
    /// No smartcards are connected.
    #[error("No smartcards are connected")]
    NoSmartcards,
    /// The key already exists.
    #[error("{0} already exists: {1}")]
    KeyExists(Keygrip, String),

    #[error(transparent)]
    Io(#[from] std::io::Error),

    #[error(transparent)]
    Utf8(#[from] std::str::Utf8Error),

    #[error(transparent)]
    Assuan(#[from] assuan::Error),

    #[error(transparent)]
    GnuPG(#[from] gnupg::Error),

    #[error(transparent)]
    KeyInfo(#[from] keyinfo::Error),

    #[error(transparent)]
    OpenPGP(#[from] openpgp::Error),

    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

type Result<R, E=Error> = std::result::Result<R, E>;

/// Controls how gpg-agent inquires passwords.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PinentryMode {
    /// Ask using pinentry.  This is the default.
    Ask,
    /// Cancel all inquiries.
    Cancel,
    /// Refuse all inquiries.
    Error,
    /// Ask the frontend (us) for passwords.
    Loopback,
}

impl Default for PinentryMode {
    fn default() -> Self {
        PinentryMode::Ask
    }
}

impl PinentryMode {
    /// Returns a string representation usable with the gpg-agent.
    pub fn as_str(&self) -> &'static str {
        match self {
            PinentryMode::Ask => "ask",
            PinentryMode::Cancel => "cancel",
            PinentryMode::Error => "error",
            PinentryMode::Loopback => "loopback",
        }
    }
}

impl std::fmt::Display for PinentryMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl std::str::FromStr for PinentryMode {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "ask" => Ok(PinentryMode::Ask),
            "default" => Ok(PinentryMode::Ask),
            "cancel" => Ok(PinentryMode::Cancel),
            "error" => Ok(PinentryMode::Error),
            "loopback" => Ok(PinentryMode::Loopback),
            _ => Err(anyhow::anyhow!("Unknown pinentry mode {:?}", s)),
        }
    }
}

fn trace_data_sent(data: &[u8]) {
    tracer!(TRACE, "trace_data_sent");
    let mut data = stfu8::encode_u8(data);
    if data.len() > 80 && data.starts_with("D ") {
        data = format!("{}... ({} bytes)",
                       data.chars().take(65).collect::<String>(),
                       data.len());
    }
    t!("SENT: {}", data);
}

fn trace_data_received(data: &[u8]) {
    tracer!(TRACE, "trace_data_received");
    let mut data = stfu8::encode_u8(data);
    if data.len() > 80 && data.starts_with("D ") {
        data = format!("{}... ({} bytes)",
                       data.chars().take(65).collect::<String>(),
                       data.len());
    }
    t!("RECV: {}", data);
}

/// A connection to a GnuPG agent.
#[derive(Debug)]
pub struct Agent {
    socket_path: PathBuf,
    c: assuan::Client,
    pinentry_mode: Option<PinentryMode>,

    /// Whether this is an restricted connection.
    restricted: bool,
}

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 futures::Stream for Agent {
    type Item = Result<assuan::Response>;

    /// 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_next(mut self: Pin<&mut Self>, cx: &mut futures::task::Context<'_>)
        -> futures::task::Poll<Option<Self::Item>>
    {
        Pin::new(&mut self.c).poll_next(cx)
    }
}

impl Agent {
    /// Connects to the `gpg-agent` serving the specified context.
    ///
    /// If a `gpg-agent` isn't listening on the specified context,
    /// this function tries to start one.
    ///
    /// The GnuPG context, [`gnupg::Context`], specifies the GnuPG
    /// home directory.
    pub async fn connect(ctx: &gnupg::Context) -> Result<Self> {
        tracer!(TRACE, "connect");

        async fn transaction(ctx: &gnupg::Context) -> Result<Agent> {
            t!("Starting daemon if not running");

            // If the home directory doesn't exist, gpg-agent won't
            // start, and we'll later get obscure errors.  Check
            // eagerly so avoid confusing the eagerly.
            let homedir = ctx.homedir();

            // Disabled on Windows.  Re-enable when this issue is fixed:
            //
            // https://gitlab.com/sequoia-pgp/sequoia/-/issues/1093
            //
            // SEE OTHER INSTANCES OF THIS BELOW (search for comment).
            #[cfg(not(windows))]
            if let Some(homedir) = homedir {
                if ! homedir.exists() {
                    return Err(Error::GnuPGHomeMissing(
                        homedir.to_path_buf()).into());
                }
            }

            // Try to start gpg-agent.
            ctx.start("gpg-agent")?;

            t!("Connecting to daemon");
            let path = ctx.socket("agent")?;
            Agent::connect_to_agent(path).await
        }

        transaction(ctx).await.map_err(|e| {
            t!("failed: {}", e);
            e
        })
    }

    /// Connects to the `gpg-agent` serving the specified context.
    ///
    /// If a `gpg-agent` isn't listening on the specified context,
    /// this function tries to start one.
    ///
    /// A default [`gnupg::Context`] with the specified home directory
    /// is used as the context.
    pub async fn connect_to<P>(homedir: P) -> Result<Self>
        where P: AsRef<Path>
    {
        let ctx = gnupg::Context::with_homedir(homedir)?;
        Self::connect(&ctx).await
    }

    /// Connects to the default `gpg-agent`.
    ///
    /// If a `gpg-agent` isn't listening on the specified context,
    /// this function tries to start one.
    ///
    /// A default [`gnupg::Context`] with the default home directory
    /// is used as the context.
    pub async fn connect_to_default() -> Result<Self>
    {
        let ctx = gnupg::Context::new()?;
        Self::connect(&ctx).await
    }

    /// Connects to a new, ephemeral `gpg-agent`.
    ///
    /// A default [`gnupg::Context`] with an ephemeral home directory
    /// is used as the context.
    pub async fn connect_to_ephemeral() -> Result<Self>
    {
        let ctx = gnupg::Context::ephemeral()?;
        Self::connect(&ctx).await
    }

    /// Connects to the `gpg-agent` listening on the specified socket.
    ///
    /// If a `gpg-agent` isn't listening on the specified socket,
    /// this function does *not* try to start one.
    pub async fn connect_to_agent<P>(socket_path: P) -> Result<Self>
        where P: AsRef<Path>
    {
        let socket_path = socket_path.as_ref();

        let mut connection = assuan::Client::connect(socket_path).await?;
        if TRACE.load(std::sync::atomic::Ordering::Relaxed) {
            connection.trace_data_sent(Box::new(trace_data_sent));
            connection.trace_data_received(Box::new(trace_data_received));
        }

        let mut agent = Agent {
            socket_path: socket_path.to_path_buf(),
            c: connection,
            pinentry_mode: None,
            restricted: false,
        };

        // Check whether this is an restricted connection.
        let restricted =
            agent.send_simple("GETINFO restricted").await.is_ok();
        agent.restricted = restricted;

        Ok(agent)
    }

    /// Returns whether this is an restricted connection.
    pub fn is_restricted(&self) -> bool {
        self.restricted
    }

    /// Tells the gpg-agent to reload its configuration file.
    pub async fn reload(&mut self) -> Result<()>
    {
        self.send_simple("RELOADAGENT").await?;
        Ok(())
    }

    /// Overrides the pinentry mode.
    pub fn set_pinentry_mode(&mut self, mode: PinentryMode) {
        self.pinentry_mode = Some(mode);
    }

    /// Disables gpg's pinentry.
    ///
    /// Changes the pinentry mode to `PinEntryMode::Error`, which
    /// configures the agent to not ask for a password.
    pub fn suppress_pinentry(mut self) -> Self {
        self.pinentry_mode = Some(PinentryMode::Error);
        self
    }

    /// Returns whether the agent has a secret key.
    pub async fn has_key(&mut self,
                         key: &Key<PublicParts, UnspecifiedRole>)
                         -> Result<bool>
    {
        let grip = Keygrip::of(key.mpis())?;
        Ok(self.send_simple(format!("HAVEKEY {}", grip)).await.is_ok())
    }

    /// Returns the keys managed by the agent.
    pub async fn list_keys(&mut self) -> Result<keyinfo::KeyInfoList> {
        self.send("KEYINFO --list")?;

        let mut keys = Vec::new();
        while let Some(response) = self.next().await {
            let response = response?;
            match response {
                Response::Ok { .. } => break,
                Response::Comment { .. } => (),
                Response::Status { ref keyword, ref message } => {
                    if keyword == "KEYINFO" {
                        keys.push(keyinfo::KeyInfo::parse(message)?)
                    } else {
                        return protocol_error(&response);
                    }
                }
                // KEYINFO should not send an inquire.
                Response::Inquire { .. } => return protocol_error(&response),
                Response::Error { ref message, .. } =>
                    return self.operation_failed(message).await,
                response =>
                    return protocol_error(&response),
            }
        }

        self.next().await; // Dummy read to send END.
        Ok(keyinfo::KeyInfoList::from_iter(keys.into_iter()))
    }

    /// Returns information about a key managed by the agent.
    pub async fn key_info(&mut self, keygrip: &Keygrip) -> Result<keyinfo::KeyInfo> {
        self.send(format!("KEYINFO {}", keygrip.to_string()))?;

        let mut keyinfo = None;
        while let Some(response) = self.next().await {
            let response = response?;
            match response {
                Response::Ok { .. } => break,
                Response::Comment { .. } => (),
                Response::Status { ref keyword, ref message } => {
                    if keyword == "KEYINFO" {
                        if keyinfo.is_none() {
                            keyinfo = Some(keyinfo::KeyInfo::parse(message)?);
                        } else {
                            // Only expect exactly one keyinfo line.
                            return protocol_error(&response);
                        }
                    } else {
                        return protocol_error(&response);
                    }
                }
                // KEYINFO should not send an inquire.
                Response::Inquire { .. } => return protocol_error(&response),
                Response::Error { ref message, .. } =>
                    return self.operation_failed(message).await,
                response =>
                    return protocol_error(&response),
            }
        }

        self.next().await; // Dummy read to send END.
        if let Some(keyinfo) = keyinfo {
            Ok(keyinfo)
        } else {
            Err(Error::UnknownKey(keygrip.clone()).into())
        }
    }

    /// Returns information about any *connected* smartcards.
    ///
    /// This sends `LEARN --sendinfo` to gpg-agent.
    pub async fn card_info(&mut self) -> Result<cardinfo::CardInfo> {
        self.send("learn --sendinfo")?;

        let mut raw: Vec<(String, String)> = Vec::new();
        while let Some(response) = self.next().await {
            let response = response?;
            match response {
                Response::Ok { .. } => break,
                Response::Comment { .. } => (),
                Response::Status { ref keyword, ref message } => {
                    // Skip progress lines; they are just informative.
                    if keyword != "PROGRESS" {
                        raw.push((keyword.clone(), message.clone()));
                    }
                }
                // KEYINFO should not send an inquire.
                Response::Inquire { .. } => return protocol_error(&response),
                Response::Error { ref message, .. } => {
                    if message.as_ref().map(|m| m.starts_with("100663406 "))
                        .unwrap_or(false)
                    {
                        // Card removed.
                        return self.operation_failed_as(
                            Error::NoSmartcards.into()).await;
                    } else {
                        return self.operation_failed(message).await;
                    }
                }
                response =>
                    return protocol_error(&response),
            }
        }

        self.next().await; // Dummy read to send END.

        if raw.is_empty() {
            Err(Error::NoSmartcards.into())
        } else {
            cardinfo::CardInfo::parse(raw)
        }
    }

    /// Imports a secret key into the agent.
    ///
    /// `key` is the secret key that will be imported.
    ///
    /// `policy` and `cert` are decorative.  They are only used to
    /// create a better password prompt.
    ///
    /// `unattended` tells `gpg-agent` whether it should import the
    /// key without reencrypting it.  If `false`, the user is prompted
    /// to decrypt the secret key material, and it is reencrypted.
    ///
    /// `overwrite` tells the agent to overwrite an existing version
    /// of the key.  If `overwrite` is not set, and a variant of the
    /// key already exists, then `Error::KeyExists` is returned.
    pub async fn import(&mut self,
                        policy: &dyn Policy,
                        cert: &Cert,
                        key: &Key<SecretParts, UnspecifiedRole>,
                        unattended: bool,
                        overwrite: bool)
                        -> Result<bool>
    {
        // The gpg-agent shipped with GnuPG 2.4.x calculates the checksum
        // over ECC artifacts differently.  Oddly, this seems to amount to
        // adding 8 to the checksum.  See GnuPG commit
        // 2b118516240b4bddd34c68c23a99bea56682a509.
        use sequoia_openpgp::types::PublicKeyAlgorithm::*;
        let mut r = self.import_int(
            policy, cert, key, unattended, overwrite, 0).await;
        if r.is_err() && (key.pk_algo() == ECDSA
                          || key.pk_algo() == EdDSA
                          || key.pk_algo() == ECDH)
        {
            r = self.import_int(
                policy, cert, key, unattended, overwrite, 8).await;
        }

        r
    }

    async fn import_int(&mut self,
                        policy: &dyn Policy,
                        cert: &Cert,
                        key: &Key<SecretParts, UnspecifiedRole>,
                        unattended: bool,
                        overwrite: bool,
                        csum_offset: u16)
                        -> Result<bool>
    {
        use ipc::sexp::*;

        let keygrip = Keygrip::of(key.mpis())?;

        /// Makes a tuple cell, i.e. a *Cons*.
        fn c(name: &str, data: &[u8]) -> Sexp {
            Sexp::List(vec![Sexp::String(name.as_bytes().into()),
                            Sexp::String(data.into())])
        }

        /// Makes a tuple cell with a string value, i.e. a *String* cons.
        fn s(name: &str, data: impl ToString) -> Sexp {
            c(name, data.to_string().as_bytes())
        }

        fn add_signed_mpi(list: &mut Vec<Sexp>, v: &[u8]) {
            let mut v = v.to_vec();

            // If the high bit is set, we need to prepend a zero byte,
            // otherwise the agent will interpret the value as signed, and
            // thus negative.
            if v[0] & 0x80 > 0 {
                v.insert(0, 0);
            }

            add_raw(list, "_", &v);
        }

        fn add(list: &mut Vec<Sexp>, mpi: &mpi::MPI) {
            add_signed_mpi(list, mpi.value());
        }
        fn addp(list: &mut Vec<Sexp>, checksum: &mut u16, mpi: &mpi::ProtectedMPI) {
            add_signed_mpi(list, mpi.value());

            use openpgp::serialize::MarshalInto;
            *checksum = checksum.wrapping_add(
                mpi.to_vec().expect("infallible").iter()
                    .fold(0u16, |acc, v| acc.wrapping_add(*v as u16)));
        }

        fn add_raw(list: &mut Vec<Sexp>, name: &str, data: &[u8]) {
            list.push(Sexp::String(name.into()));
            list.push(Sexp::String(data.into()));
        }

        use openpgp::crypto::mpi::{self, PublicKey};
        let mut skey = vec![Sexp::String("skey".into())];
        let curve = match key.mpis() {
            PublicKey::RSA { e, n, } => {
                add(&mut skey, n);
                add(&mut skey, e);
                None
            },
            PublicKey::DSA { p, q, g, y, } => {
                add(&mut skey, p);
                add(&mut skey, q);
                add(&mut skey, g);
                add(&mut skey, y);
                None
            },
            PublicKey::ElGamal { p, g, y, } => {
                add(&mut skey, p);
                add(&mut skey, g);
                add(&mut skey, y);
                None
            },
            PublicKey::EdDSA { curve, q, }
            | PublicKey::ECDSA { curve, q, }
            | PublicKey::ECDH { curve, q, .. } => {
                add(&mut skey, q);
                Some(curve.clone())
            },
            PublicKey::Unknown { mpis, rest, } => {
                for m in mpis.iter() {
                    add(&mut skey, m);
                }
                add_raw(&mut skey, "_", rest);
                None
            },
            _ => return
                Err(openpgp::Error::UnsupportedPublicKeyAlgorithm(key.pk_algo())
                    .into()),
        };

        // Now we append the secret bits.  We also compute a checksum over
        // the MPIs.
        let mut checksum = 0u16;
        let protection = match key.secret() {
            SecretKeyMaterial::Encrypted(e) => {
                let mut p =
                    vec![Sexp::String("protection".into())];
                p.push(Sexp::String(match e.checksum() {
                    Some(SecretKeyChecksum::SHA1) => "sha1",
                    Some(SecretKeyChecksum::Sum16) => "sum",
                    None => "none", // XXX: does that happen?
                }.into()));
                p.push(Sexp::String(babel::Fish(e.algo()).to_string().as_str().into()));

                let iv_len = e.algo().block_size().unwrap_or(0);
                let iv = e.ciphertext().map(|c| &c[..iv_len.min(c.len())])
                    .unwrap_or(&[]);
                p.push(Sexp::String(iv.into()));

                #[allow(deprecated)]
                match e.s2k() {
                    S2K::Iterated { hash, salt, hash_bytes, } => {
                        p.push(Sexp::String("3".into()));
                        p.push(Sexp::String(babel::Fish(*hash).to_string().as_str().into()));
                        p.push(Sexp::String(salt[..].into()));
                        p.push(Sexp::String(
                            utils::s2k_encode_iteration_count(*hash_bytes)
                                .unwrap_or_default().to_string().as_str().into()));
                    },
                    S2K::Salted { hash, salt } => {
                        p.push(Sexp::String("1".into()));
                        p.push(Sexp::String(babel::Fish(*hash).to_string().as_str().into()));
                        p.push(Sexp::String(salt[..].into()));
                        p.push(Sexp::String("0".into()));
                    },
                    S2K::Simple { hash } => {
                        p.push(Sexp::String("0".into()));
                        p.push(Sexp::String(babel::Fish(*hash).to_string().as_str().into()));
                        p.push(Sexp::String([][..].into()));
                        p.push(Sexp::String("0".into()));
                    },
                    S2K::Private { .. } | S2K::Unknown { .. } | _ => {
                        return Err(anyhow::anyhow!("Unsupported protection mode").into());
                    },
                }

                if let Ok(c) = e.ciphertext() {
                    skey.push(Sexp::String("e".into()));
                    // We must omit the IV here.
                    skey.push(Sexp::String(c[iv_len.min(c.len())..].into()));
                } else {
                    return Err(anyhow::anyhow!("Failed to parse ciphertext").into());
                }

                Sexp::List(p)
            },
            SecretKeyMaterial::Unencrypted(u) => {
                u.map(|s| match s {
                    mpi::SecretKeyMaterial::RSA { d, p, q, u, } => {
                        addp(&mut skey, &mut checksum, d);
                        addp(&mut skey, &mut checksum, p);
                        addp(&mut skey, &mut checksum, q);
                        addp(&mut skey, &mut checksum, u);
                    },
                    mpi::SecretKeyMaterial::DSA { x, }
                    | mpi::SecretKeyMaterial::ElGamal { x, } =>
                        addp(&mut skey, &mut checksum, x),
                    mpi::SecretKeyMaterial::EdDSA { scalar, }
                    | mpi::SecretKeyMaterial::ECDSA { scalar, }
                    | mpi::SecretKeyMaterial::ECDH { scalar, } =>
                        addp(&mut skey, &mut checksum, scalar),
                    mpi::SecretKeyMaterial::Unknown { mpis, rest, } => {
                        for m in mpis.iter() {
                            addp(&mut skey, &mut checksum, m);
                        }
                        add_raw(&mut skey, "_", rest);
                        checksum = checksum.wrapping_add(
                            rest.iter()
                                .fold(0u16, |acc, v| acc.wrapping_add(*v as u16)));
                    },
                    _ => (), // XXX This will fail anyway.
                });
                s("protection", "none")
            },
        };

        let mut transfer_key = vec![
            Sexp::String("openpgp-private-key".into()),
            s("version", key.version()),
            s("algo", babel::Fish(key.pk_algo())), // XXX does that map correctly?
        ];
        if let Some(curve) = curve {
            transfer_key.push(s("curve", curve.to_string()));
        }
        transfer_key.push(Sexp::List(skey));
        transfer_key.push(s("csum", checksum.wrapping_add(csum_offset)));
        transfer_key.push(protection);

        let transfer_key = Sexp::List(transfer_key);

        // Pad to a multiple of 64 bits so that we can AESWRAP it.
        let mut buf = Vec::new();
        transfer_key.serialize(&mut buf)?;
        while buf.len() % 8 > 0 {
            buf.push(0);
        }
        let padded_transfer_key = Protected::from(buf);

        self.send_simple(
            format!("SETKEYDESC {}",
                    escape(Self::make_import_prompt(policy, cert, key)))).await?;

        // Get the Key Encapsulation Key for transferring the key.
        let kek = self.send_simple("KEYWRAP_KEY --import").await?;

        // Now encrypt the key.
        let encrypted_transfer_key = openpgp::crypto::ecdh::aes_key_wrap(
            openpgp::types::SymmetricAlgorithm::AES128,
            &kek,
            &padded_transfer_key)?;
        assert_eq!(padded_transfer_key.len() + 8, encrypted_transfer_key.len());

        // Did we import it?
        let mut imported = false;

        // And send it!
        if let Some(mode) = self.pinentry_mode.as_ref().map(|m| m.to_string()) {
            self.send_simple(
                format!("OPTION pinentry-mode={}", mode)).await?;
        }
        self.send(format!("IMPORT_KEY --timestamp={}{}{}",
                           chrono::DateTime::<chrono::Utc>::from(key.creation_time())
                           .format("%Y%m%dT%H%M%S"),
                           if unattended { " --unattended" } else { "" },
                           if overwrite { " --force" } else { "" },
        ))?;
        while let Some(response) = self.next().await {
            match response? {
                Response::Ok { .. }
                | Response::Comment { .. }
                | Response::Status { .. } =>
                    (), // Ignore.
                Response::Inquire { keyword, .. } => {
                    match keyword.as_str() {
                        "KEYDATA" => {
                            self.data(&encrypted_transfer_key)?;
                            // Dummy read to send data.
                            self.next().await;

                            // Then, handle the inquiry.
                            while let Some(r) = self.next().await {
                                match r? {
                                    // May send CACHE_NONCE
                                    Response::Status { .. } =>
                                        (), // Ignore.
                                    Response::Ok { .. } => {
                                        imported = true;
                                        break;
                                    },
                                    // May send PINENTRY_LAUNCHED when
                                    // importing locked keys.
                                    Response::Inquire { .. } =>
                                        self.acknowledge_inquiry().await?,
                                    Response::Error { code, message } => {
                                        match code {
                                            0x4008023 => {
                                                // Key exists.
                                                return self.operation_failed_as(
                                                    Error::KeyExists(
                                                        keygrip.clone(),
                                                        message.unwrap_or_else(|| {
                                                            "key exists".into()
                                                        })).into()
                                                ).await;
                                            }
                                            _ => {
                                                return self.operation_failed(
                                                    &message).await;
                                            },
                                        }
                                    },
                                    response =>
                                        return protocol_error(&response),
                                }
                            }

                            // Sending the data acknowledges the inquiry.
                        },
                        _ => self.acknowledge_inquiry().await?,
                    }
                },
                Response::Error { ref message, .. } =>
                    return self.operation_failed(message).await,
                response =>
                    return protocol_error(&response),
            }
        }

        Ok(imported)
    }

    fn make_import_prompt(policy: &dyn Policy, cert: &Cert,
                          key: &Key<SecretParts, UnspecifiedRole>)
                          -> String
    {
        let primary_id = cert.keyid();
        let keyid = key.keyid();
        let uid = utils::best_effort_primary_uid(policy, cert);

        match (primary_id == keyid, Some(uid)) {
            (true, Some(uid)) => format!(
                "Please enter the passphrase to \
                 unlock the OpenPGP secret key:\n\
                 {}\n\
                 ID {:X}, created {}.",
                uid,
                keyid,
                Timestamp::try_from(key.creation_time())
                    .expect("creation time is representable"),
            ),
            (false, Some(uid)) => format!(
                "Please enter the passphrase to \
                 unlock the OpenPGP secret key:\n\
                 {}\n\
                 ID {:X}, created {} (main key ID {}).",
                uid,
                keyid,
                Timestamp::try_from(key.creation_time())
                    .expect("creation time is representable"),
                primary_id,
            ),
            (true, None) => format!(
                "Please enter the passphrase to \
                 unlock the OpenPGP secret key:\n\
                 ID {:X}, created {}.",
                keyid,
                Timestamp::try_from(key.creation_time())
                    .expect("creation time is representable"),
            ),
            (false, None) => format!(
                "Please enter the passphrase to \
                 unlock the OpenPGP secret key:\n\
                 ID {:X}, created {} (main key ID {}).",
                keyid,
                Timestamp::try_from(key.creation_time())
                    .expect("creation time is representable"),
                primary_id,
            ),
        }
    }

    /// Presets the password for a key.
    ///
    /// This does not check that the password is correct.
    ///
    /// This will fail if the user has not enabled presetting
    /// passwords.  In particular, the user must explicitly opt-in by
    /// adding `allow-preset-passphrase` to their `gpg-agent.conf`.
    /// If this is not set, which will usually be the case since it is
    /// not the default, this operation will fail and return
    /// [`assuan::Error::OperationFailed`].
    pub async fn preset_passphrase(&mut self,
                                   keygrip: &Keygrip,
                                   password: Password)
        -> Result<()>
    {
        let escaped = password
            .map(|p| {
                p.iter().map(|b| format!("{:02X}", b))
                    .collect::<String>()
            });
        self.send_simple(
            format!("PRESET_PASSPHRASE {} -1 {}",
                    keygrip, escaped)).await
            .map_err(|err| {
                err
            })?;

        Ok(())
    }

    /// Makes the agent ask for a password.
    ///
    /// The callback is invoked once for each response.  If the
    /// callback returns an error, any outstanding inquiry is
    /// acknowledged, and the error is returned.
    ///
    /// The result of the callback is only used if gpg-agent sent an
    /// inquiry.  Otherwise, the result is silently ignored.
    ///
    /// If the response to an inquiry is `Ok(None)`, the inquiry is
    /// acknowledged, and this function waits for another response.
    pub async fn get_passphrase<P>(&mut self,
                                   cache_id: &Option<String>,
                                   err_msg: &Option<String>,
                                   prompt: Option<String>,
                                   desc_msg: Option<String>,
                                   newsymkey: bool,
                                   repeat: usize,
                                   check: bool,
                                   qualitybar: bool,
                                   mut pinentry_cb: P)
                                   -> Result<Password>
    where
        P: FnMut(&mut Agent, Response) -> Result<Option<Protected>>,
    {
        self.send(format!(
            "GET_PASSPHRASE --data --repeat={}{}{}{} -- {} {} {} {}",
            repeat,
            if (repeat > 0 && check) || newsymkey { " --check" } else { "" },
            if qualitybar { " --qualitybar" } else { "" },
            if newsymkey { " --newsymkey" } else { "" },
            cache_id.as_ref().map(escape).unwrap_or_else(|| "X".into()),
            err_msg.as_ref().map(escape).unwrap_or_else(|| "X".into()),
            prompt.as_ref().map(escape).unwrap_or_else(|| "X".into()),
            desc_msg.as_ref().map(escape).unwrap_or_else(|| "X".into()),
        ))?;

        let mut password = Vec::new();
        while let Some(response) = self.next().await {
            match response? {
                r @ Response::Ok { .. }
                | r @ Response::Comment { .. }
                | r @ Response::Status { .. } => {
                    pinentry_cb(self, r)?;
                },
                r @ Response::Inquire { .. } => {
                    match pinentry_cb(self, r) {
                        Ok(Some(data)) => {
                            self.data(&data[..])?;
                            // Dummy read to send data.
                            while let Some(r) = self.next().await {
                                if matches!(r?, Response::Ok { .. }) {
                                    break;
                                }
                            }

                            // Sending the data acknowledges the inquiry.
                        }
                        Ok(None) => {
                            self.acknowledge_inquiry().await?;
                        }
                        Err(err) => {
                            self.acknowledge_inquiry().await?;
                            return Err(err);
                        }
                    }
                },
                Response::Data { partial } => {
                    // Securely erase partial.
                    let partial = Protected::from(partial);
                    password.extend_from_slice(&partial);
                },
                Response::Error { ref message, .. } =>
                    return self.operation_failed(message).await,
            }
        }
        let password = Password::from(password);

        Ok(password)
    }

    /// Makes the agent forget a password.
    ///
    /// The cache_id is usually a keygrip, but gpg-agent can also
    /// cache the password for a SKESK.
    ///
    /// When a passphrase is cleared, `gpg-agent` launches a pinentry
    /// and sends it the `CLEARPASSPHRASE` command so that it also
    /// flushes the passphrase from its cache, if any.  You can see
    /// whether gpg does that by providing a callback.  It will be
    /// called for `PINENTRY_LAUNCHED` inquire line.  Most of the time
    /// you won't care and can just pass `|_| ()`.
    pub async fn forget_passphrase<C, P>(&mut self,
                                         cache_id: C,
                                         mut pinentry_cb: P)
                                         -> Result<()>
    where
        C: AsRef<str>,
        P: FnMut(Vec<u8>),
    {
        self.send(format!("CLEAR_PASSPHRASE {}", escape(cache_id.as_ref())))?;
        while let Some(response) = self.next().await {
            match response? {
                Response::Ok { .. }
                | Response::Comment { .. }
                | Response::Status { .. } =>
                    (), // Ignore.
                Response::Inquire { keyword, parameters } => {
                    match keyword.as_str() {
                        "PINENTRY_LAUNCHED" => {
                            pinentry_cb(parameters.unwrap_or_default());
                        },
                        _ => (),
                    }
                    self.acknowledge_inquiry().await?
                },
                Response::Error { ref message, .. } =>
                    return self.operation_failed(message).await,
                response =>
                    return protocol_error(&response),
            }
        }
        Ok(())
    }

    /// Helper function to respond to inquiries.
    ///
    /// This function doesn't do something useful, but it ends the
    /// current inquiry.
    async fn acknowledge_inquiry(&mut self) -> Result<()> {
        self.send("END")?;
        self.next().await; // Dummy read to send END.
        Ok(())
    }

    /// Returns a convenient Err value for use in the state machines.
    ///
    /// This function must only be called after the assuan server
    /// returns an ERR.  message is the error message returned from
    /// the server.  This function first checks that the server hasn't
    /// sent anything else, which would be a protocol violation.  If
    /// that is not the case, it turns the message into an Err.
    async fn operation_failed<T>(&mut self, message: &Option<String>)
                                 -> Result<T>
    {
        self.operation_failed_as(
            assuan::Error::OperationFailed(
                message.as_ref().map(|e| e.to_string())
                    .unwrap_or_else(|| "Unknown reason".into()))
                .into()).await
    }

    async fn operation_failed_as<T>(&mut self, err: Error)
                                    -> Result<T>
    {
        tracer!(TRACE, "operation_failed_as");

        if let Some(response) = self.next().await {
            t!("Got unexpected response {:?}", response);
            Err(assuan::Error::ProtocolError(
                format!("Got unexpected response {:?}", response))
                .into())
        } else {
            t!("Operation failed: {}", err);
            Err(err)
        }
    }

    /// Computes options that we want to communicate.
    fn options(&self) -> Vec<String> {
        use std::env::var;

        if self.is_restricted() {
            // We cannot set any of these options on restricted
            // connections.
            return vec![];
        }

        let mut r = Vec::new();

        if let Ok(tty) = var("GPG_TTY") {
            r.push(format!("OPTION ttyname={}", tty));
        } else {
            #[cfg(unix)]
            unsafe {
                use std::ffi::CStr;
                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
    }

    /// Returns a `KeyPair` for `key` with the secret bits managed by
    /// the agent.
    ///
    /// `KeyPair` implements `crypto::Signer` and `crypto::Decryptor`.
    /// This makes it easy for code to use secret key material managed
    /// by an instance of gpg agent from code that uses Sequoia.
    ///
    /// This function does not check that the agent actually manages
    /// the secret key material.  If the agent doesn't manage the
    /// secret key material, operations that attempt to use it will
    /// fail.  If necessary, you can use [`Agent::has_key`] to check
    /// if the agent manages the secret key material.
    pub fn keypair<R>(&self, key: &Key<PublicParts, R>)
        -> Result<KeyPair>
        where R: KeyRole
    {
        let mut pair = KeyPair::new_for_socket(&self.socket_path, key)?;
        pair.pinentry_mode = self.pinentry_mode.clone();
        Ok(pair)
    }

    /// Exports a secret key from the agent.
    ///
    /// `key` is the secret key that will be exported.
    pub async fn export(&mut self, key: Key<PublicParts, UnspecifiedRole>)
        -> Result<Key<SecretParts, UnspecifiedRole>>
    {
        let keygrip = Keygrip::of(key.mpis())?;

        let kek = self.send_simple("KEYWRAP_KEY --export").await?;

        let encrypted_key = self.send_simple(
            format!("EXPORT_KEY {}", keygrip.to_string())).await?;

        let secret_key = openpgp::crypto::ecdh::aes_key_unwrap(
            openpgp::types::SymmetricAlgorithm::AES128,
            &kek,
            &encrypted_key.as_ref())?;

        // Strip any trailing NULs.  They are only there for padding
        // purposes.
        let mut secret_key = secret_key.as_ref();
        while ! secret_key.is_empty() && secret_key[secret_key.len() - 1] == 0 {
            secret_key = &secret_key[..secret_key.len() - 1];
        }

        let sexp = Sexp::from_bytes(secret_key)?;

        let secret_key = sexp.to_secret_key(Some(key.mpis()))?;

        let (key, _) = key.add_secret(secret_key.into());

        Ok(key)
    }
}

/// Returns a convenient Err value for use in the state machines
/// below.
fn protocol_error<T>(response: &Response) -> Result<T> {
    tracer!(TRACE, "operation_failed");

    t!("Got unexpected response {:?}", response);
    Err(assuan::Error::ProtocolError(
        format!("Got unexpected response {:?}", response))
        .into())
}

/// Computes the cache id for a SKESK.
///
/// If an S2K algorithm unsupported by the caching id algorithm is
/// given, this function returns `None`.
pub fn cacheid_of(s2k: &S2K) -> Option<String> {
    #[allow(deprecated)]
    let salt = match s2k {
        S2K::Iterated { salt, .. } => &salt[..8],
        S2K::Salted { salt, .. } => &salt[..8],
        _ => return None,
    };

    Some(format!("S{}", openpgp::fmt::hex::encode(&salt)))
}

/// Computes the cache id for a set of SKESKs.
///
/// GnuPG prompts for a password for each SKESK separately, and uses
/// the first eight bytes of salt from the S2K.  We ask for one
/// password and try it with every SKESK.  Therefore, we have to cache
/// that we asked for a set of SKESKs, i.e. this message.  To that
/// end, we xor the first eight bytes of salt from every S2K, matching
/// GnuPG's result in the common case of having just one SKESK.  Xor
/// is also nice because it is commutative, so the order of the SKESKs
/// doesn't matter.
///
/// Unsupported SKESK versions or S2K algorithms unsupported by the
/// caching id algorithm are ignored.  We cannot use them anyway.
///
/// Further, if no SKESKs are given, this function returns `None`.
pub fn cacheid_over_all(skesks: &[SKESK]) -> Option<String> {
    if skesks.is_empty() {
        return None;
    }

    let mut cacheid = [0; 8];

    for skesk in skesks {
        let s2k = match skesk {
            SKESK::V4(skesk) => skesk.s2k(),
            _ => continue,
        };

        #[allow(deprecated)]
        let salt = match s2k {
            S2K::Iterated { salt, .. } => &salt[..8],
            S2K::Salted { salt, .. } => &salt[..8],
            _ => continue,
        };

        cacheid.iter_mut().zip(salt.iter()).for_each(|(p, s)| *p ^= *s);
    }

    Some(format!("S{}", openpgp::fmt::hex::encode(&cacheid)))
}

/// 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`.
///
/// This provides a convenient, synchronous interface for use with the
/// low-level Sequoia crate.
pub struct KeyPair {
    public: Key<PublicParts, UnspecifiedRole>,
    agent_socket: PathBuf,
    password: Option<crypto::Password>,
    pinentry_mode: Option<PinentryMode>,
    password_prompt: String,
    delete_prompt: String,
}

impl KeyPair {
    fn default_password_prompt(key: &Key<PublicParts, UnspecifiedRole>)
        -> String
    {
        format!(
            "Please enter the passphrase to \
             unlock the OpenPGP secret key:\n\
             ID {:X}, created {}.",
            key.keyid(), Timestamp::try_from(key.creation_time()).unwrap())
    }

    fn default_delete_prompt(key: &Key<PublicParts, UnspecifiedRole>)
        -> String
    {
        format!(
            "Do you really want to permanently delete the OpenPGP secret key:\n\
             ID {:X}, created {}.",
            key.keyid(), Timestamp::try_from(key.creation_time()).unwrap())
    }

    /// 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_for_gnupg_context<R>(ctx: &gnupg::Context,
                                    key: &Key<PublicParts, R>)
        -> Result<KeyPair>
        where R: KeyRole
    {
        let key = key.role_as_unspecified();
        Ok(KeyPair {
            password: None,
            pinentry_mode: None,
            password_prompt: Self::default_password_prompt(key),
            delete_prompt: Self::default_delete_prompt(key),
            public: key.clone(),
            agent_socket: ctx.socket("agent")?.into(),
        })
    }

    /// Returns a `KeyPair` for `key` with the secret bits managed by
    /// the agent.
    ///
    /// If you have a [`Agent`], then you should create a `KeyPair`
    /// using [`Agent::keypair`].
    pub fn new_for_socket<P, R>(agent_socket: P, key: &Key<PublicParts, R>)
        -> Result<KeyPair>
        where P: AsRef<Path>,
              R: KeyRole
    {
        let key = key.role_as_unspecified();
        Ok(KeyPair {
            password: None,
            pinentry_mode: None,
            password_prompt: Self::default_password_prompt(key),
            delete_prompt: Self::default_delete_prompt(key),
            public: key.clone(),
            agent_socket: agent_socket.as_ref().to_path_buf(),
        })
    }

    /// Changes the password prompt to include information about the
    /// cert.
    ///
    /// Use this function to give more context to the user when she is
    /// prompted for a password.  This function will generate a prompt
    /// that is very similar to the prompts that GnuPG generates.
    ///
    /// To set an arbitrary password prompt, use
    /// [`KeyPair::with_password_prompt`].
    pub fn with_cert(self, cert: &ValidCert) -> Self {
        let primary_id = cert.keyid();
        let keyid = self.public.keyid();
        let password_prompt = match (primary_id == keyid,
                                     cert.primary_userid()
                                     .map(|uid| uid.clone())
                                     .ok())
        {
            (true, Some(uid)) => format!(
                "Please enter the passphrase to \
                 unlock the OpenPGP secret key:\n\
                 {}\n\
                 ID {:X}, created {}.",
                uid.userid(),
                keyid,
                Timestamp::try_from(self.public.creation_time())
                    .expect("creation time is representable"),
            ),
            (false, Some(uid)) => format!(
                "Please enter the passphrase to \
                 unlock the OpenPGP secret key:\n\
                 {}\n\
                 ID {:X}, created {} (main key ID {}).",
                uid.userid(),
                keyid,
                Timestamp::try_from(self.public.creation_time())
                    .expect("creation time is representable"),
                primary_id,
            ),
            (true, None) => format!(
                "Please enter the passphrase to \
                 unlock the OpenPGP secret key:\n\
                 ID {:X}, created {}.",
                keyid,
                Timestamp::try_from(self.public.creation_time())
                    .expect("creation time is representable"),
            ),
            (false, None) => format!(
                "Please enter the passphrase to \
                 unlock the OpenPGP secret key:\n\
                 ID {:X}, created {} (main key ID {}).",
                keyid,
                Timestamp::try_from(self.public.creation_time())
                    .expect("creation time is representable"),
                primary_id,
            ),
        };

        let delete_prompt = match cert.primary_userid()
                                      .map(|uid| uid.clone())
                                      .ok()
        {
            Some(uid) => format!(
                "Do you really want to permanently delete the OpenPGP secret key:\n\
                 {}\n\
                 ID {:X}, created {}.",
                uid.userid(),
                keyid,
                Timestamp::try_from(self.public.creation_time())
                    .expect("creation time is representable"),
            ),
            None => format!(
                "Do you really want to permanently delete the OpenPGP secret key:\n\
                 ID {:X}, created {}.",
                keyid,
                Timestamp::try_from(self.public.creation_time())
                    .expect("creation time is representable"),
            ),
        };

        self.with_password_prompt(password_prompt)
            .with_delete_prompt(delete_prompt)
    }

    /// Supplies a password to unlock the secret key.
    ///
    /// This will be used when the secret key operation is performed,
    /// e.g. when signing or decrypting a message.
    ///
    /// Note: This is the equivalent of GnuPG's
    /// `--pinentry-mode=loopback` and requires explicit opt-in in the
    /// gpg-agent configuration using the `allow-loopback-pinentry`
    /// option.  If this is not enabled in the agent, the secret key
    /// operation will fail.  It is likely only useful during testing.
    pub fn with_password(mut self, password: crypto::Password) -> Self {
        self.password = Some(password);
        self
    }

    /// Overrides the pinentry mode.
    pub fn set_pinentry_mode(mut self, mode: PinentryMode) -> Self {
        self.pinentry_mode = Some(mode);
        self
    }

    /// Disables gpg's pinentry.
    ///
    /// Changes the pinentry mode to `PinEntryMode::Error`, which
    /// configures the agent to not ask for a password.
    pub fn suppress_pinentry(mut self) -> Self {
        self.pinentry_mode = Some(PinentryMode::Error);
        self
    }

    /// Changes the password prompt.
    ///
    /// Use this function to give more context to the user when she is
    /// prompted for a password.
    ///
    /// To set an password prompt that uses information from the
    /// OpenPGP certificate, use [`KeyPair::with_cert`].
    pub fn with_password_prompt(mut self, prompt: String) -> Self {
        self.password_prompt = prompt;
        self
    }

    /// Changes the delete prompt.
    ///
    /// Use this function to give more context to the user when she is
    /// prompted to delete a key.
    ///
    /// To set an password prompt that uses information from the
    /// OpenPGP certificate, use [`KeyPair::with_cert`].
    pub fn with_delete_prompt(mut self, prompt: String) -> Self {
        self.delete_prompt = prompt;
        self
    }

    /// Changes the key's password.
    ///
    /// If `preset_password` is true, then the password will also be
    /// added to the password cache.
    pub async fn password(&mut self, preset_password: bool)
        -> Result<()>
    {
        let keygrip = Keygrip::of(self.public.mpis())?;

        // Connect to the agent.
        let mut agent = Agent::connect_to_agent(&self.agent_socket).await?;

        for option in agent.options() {
            agent.send_simple(option).await?;
        }

        if let Some(mode) = agent.pinentry_mode.clone() {
            agent.send_simple(
                format!("OPTION pinentry-mode={}", mode.as_str())).await?;
        }
        agent.send_simple(
            format!("SETKEYDESC {}",
                    assuan::escape(&self.password_prompt))).await?;

        agent.send_simple(
            format!("PASSWD {}{}",
                    if preset_password {
                        "--preset "
                    } else {
                        ""
                    },
                    keygrip.to_string()))
            .await?;

        Ok(())
    }

    /// Deletes the specified key.
    ///
    /// If `stub_only` is true, then the key will be removed if it is
    /// a stub that corresponds to a key on a token.
    pub async fn delete_key(&mut self, stub_only: bool)
        -> Result<()>
    {
        let keygrip = Keygrip::of(self.public.mpis())?;

        // Connect to the agent.
        let mut agent = Agent::connect_to_agent(&self.agent_socket).await?;

        for option in agent.options() {
            agent.send_simple(option).await?;
        }

        if let Some(mode) = agent.pinentry_mode.clone() {
            agent.send_simple(
                format!("OPTION pinentry-mode={}", mode.as_str())).await?;
        }
        agent.send_simple(
            format!("SETKEYDESC {}",
                    assuan::escape(&self.delete_prompt))).await?;

        agent.send_simple(
            format!("DELETE_KEY {}{}",
                    if stub_only {
                        "--stub-only "
                    } else {
                        ""
                    },
                    keygrip.to_string()))
            .await?;

        Ok(())
    }
}

impl KeyPair {
    /// Signs a message.
    ///
    /// An async implementation of
    /// [`sequoia_openpgp::crypto::Signer::sign`].
    pub async fn sign_async(&mut self, hash_algo: HashAlgorithm, digest: &[u8])
        -> openpgp::Result<openpgp::crypto::mpi::Signature>
    {
        // Connect to the agent and do the signing
        // operation.
        let mut agent = Agent::connect_to_agent(&self.agent_socket).await?;

        for option in agent.options() {
            agent.send_simple(option).await?;
        }

        if self.password.is_some() {
            agent.send_simple("OPTION pinentry-mode=loopback").await?;
        } else if let Some(ref mode) = self.pinentry_mode {
            agent.send_simple(
                format!("OPTION pinentry-mode={}", mode.as_str())).await?;
        }

        let grip = Keygrip::of(self.public.mpis())?;
        agent.send_simple(format!("SIGKEY {}", grip)).await?;
        agent.send_simple(
            format!("SETKEYDESC {}",
                    assuan::escape(&self.password_prompt))).await?;

        let algo = u8::from(hash_algo);
        let digest = hex::encode(&digest);
        agent.send_simple(format!("SETHASH {} {}", algo, digest)).await?;
        agent.send("PKSIGN")?;

        let mut data = Vec::new();
        while let Some(r) = agent.next().await {
            match r? {
                assuan::Response::Ok { .. }
                | assuan::Response::Comment { .. }
                | assuan::Response::Status { .. } =>
                    (), // Ignore.
                assuan::Response::Inquire { keyword, .. } =>
                    match (keyword.as_str(), &self.password) {
                        ("PASSPHRASE", Some(p)) => {
                            p.map(|p| agent.data(p))?;
                            // Dummy read to send the data.
                            agent.next().await;
                        },
                        _ => agent.acknowledge_inquiry().await?,
                    },
                assuan::Response::Error { ref message, .. } =>
                    return Ok(agent.operation_failed(message).await?),
                assuan::Response::Data { ref partial } =>
                    data.extend_from_slice(partial),
            }
        }

        Sexp::from_bytes(&data)?.to_signature()
    }
}

impl crypto::Signer for KeyPair {
    fn public(&self) -> &Key<PublicParts, UnspecifiedRole> {
        &self.public
    }

    fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8])
        -> openpgp::Result<openpgp::crypto::mpi::Signature>
    {
        use tokio::runtime::{Handle, Builder};

        // See if the current thread is managed by a tokio
        // runtime.
        if Handle::try_current().is_err() {
            // Doesn't seem to be the case, so it is safe
            // to create a new runtime and block.
            let rt = Builder::new_current_thread()
                .enable_io()
                .build()?;
            rt.block_on(self.sign_async(hash_algo, digest))
        } else {
            // It is!  We must not create a second runtime
            // on this thread, but instead we will
            // delegate this to a new thread.
            std::thread::scope(|s| {
                s.spawn(move || {
                    let rt = Builder::new_current_thread()
                        .enable_io()
                        .build()?;
                    rt.block_on(self.sign_async(hash_algo, digest))
                }).join()
            }).map_err(map_panic)?
        }
    }
}

impl KeyPair {
    /// Decrypts a message.
    ///
    /// An async implementation of
    /// [`sequoia_openpgp::crypto::Decryptor::decrypt`].
    pub async fn decrypt_async(&mut self, ciphertext: &crypto::mpi::Ciphertext,
                               plaintext_len: Option<usize>)
        -> openpgp::Result<crypto::SessionKey>
    {
        // Connect to the agent and do the decryption operation.
        let mut agent = Agent::connect_to_agent(&self.agent_socket).await?;

        for option in agent.options() {
            agent.send_simple(option).await?;
        }

        if self.password.is_some() {
            agent.send_simple("OPTION pinentry-mode=loopback").await?;
        } else if let Some(ref mode) = self.pinentry_mode {
            agent.send_simple(
                format!("OPTION pinentry-mode={}", mode.as_str())).await?;
        }

        let grip = Keygrip::of(self.public.mpis())?;
        agent.send_simple(format!("SETKEY {}", grip)).await?;
        agent.send_simple(format!("SETKEYDESC {}",
                                  assuan::escape(&self.password_prompt))).await?;
        agent.send("PKDECRYPT")?;
        let mut padding = true;
        let mut data = Vec::new();
        while let Some(r) = agent.next().await {
            match r? {
                assuan::Response::Ok { .. } |
                assuan::Response::Comment { .. } =>
                    (), // Ignore.
                assuan::Response::Inquire { ref keyword, .. } =>
                    match (keyword.as_str(), &self.password) {
                        ("PASSPHRASE", Some(p)) => {
                            p.map(|p| agent.data(p))?;
                            // Dummy read to send the data.
                            agent.next().await;
                        },
                        ("CIPHERTEXT", _) => {
                            let mut buf = Vec::new();
                            Sexp::try_from(ciphertext)?.serialize(&mut buf)?;
                            agent.data(&buf)?;
                            // Dummy read to send the data.
                            agent.next().await;
                        },
                        _ => agent.acknowledge_inquiry().await?,
                    },
                assuan::Response::Status { ref keyword, ref message } =>
                    if keyword == "PADDING" {
                        padding = message != "0";
                    },
                assuan::Response::Error { ref message, .. } =>
                    return Ok(agent.operation_failed(message).await?),
                assuan::Response::Data { ref partial } =>
                    data.extend_from_slice(partial),
            }
        }

        // 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);
        }

        Sexp::from_bytes(&data)?.finish_decryption(
            &self.public, ciphertext, plaintext_len, padding)
    }
}

impl crypto::Decryptor for KeyPair {
    fn public(&self) -> &Key<PublicParts, UnspecifiedRole> {
        &self.public
    }

    fn decrypt(&mut self, ciphertext: &crypto::mpi::Ciphertext,
               plaintext_len: Option<usize>)
        -> openpgp::Result<crypto::SessionKey>
    {
        use tokio::runtime::{Handle, Builder};

        // See if the current thread is managed by a tokio
        // runtime.
        if Handle::try_current().is_err() {
            // Doesn't seem to be the case, so it is safe
            // to create a new runtime and block.
            let rt = Builder::new_current_thread()
                .enable_io()
                .build()?;
            rt.block_on(self.decrypt_async(ciphertext, plaintext_len))
        } else {
            // It is!  We must not create a second runtime
            // on this thread, but instead we will
            // delegate this to a new thread.
            std::thread::scope(|s| {
                s.spawn(move || {
                    let rt = Builder::new_current_thread()
                        .enable_io()
                        .build()?;
                    rt.block_on(self.decrypt_async(ciphertext, plaintext_len))
                }).join()
            }).map_err(map_panic)?
        }
    }
}

/// Maps a panic of a worker thread to an error.
///
/// Unfortunately, there is nothing useful to do with the error, but
/// returning a generic error is better than panicking.
fn map_panic(_: Box<dyn std::any::Any + std::marker::Send>) -> anyhow::Error
{
    anyhow::anyhow!("worker thread panicked")
}

#[cfg(test)]
mod test {
    use super::*;

    use std::fs::File;
    use std::io::Write;

    use anyhow::Context;

    use openpgp::{
        Cert,
        crypto::{
            hash::Digest,
            mpi::Ciphertext,
            Decryptor,
            Signer,
        },
        policy::StandardPolicy,
    };

    use ipc::Keygrip;

    const P: &StandardPolicy = &StandardPolicy::new();

    async fn import_keys(agent: &mut Agent, cert: &Cert) -> Result<()> {
        assert!(cert.is_tsk(),
                "{} does not contain secret key material", cert.fingerprint());

        for k in cert.keys().secret() {
            agent.import(P,
                         &cert, k.parts_as_secret().expect("have secret"),
                         true, true).await?;
        }

        Ok(())
    }

    async fn import_testy_new(agent: &mut Agent) -> Result<Cert> {
        let cert = Cert::from_bytes(crate::tests::key("testy-new-private.pgp"))?;
        import_keys(agent, &cert).await?;
        Ok(cert)
    }

    /// Asserts that a <KeyPair as Signer> is usable from an async
    /// context.
    ///
    /// Previously, the test died with
    ///
    ///     thread 'gnupg::tests::signer_in_async_context' panicked at
    ///     'Cannot start a runtime from within a runtime. This
    ///     happens because a function (like `block_on`) attempted to
    ///     block the current thread while the thread is being used to
    ///     drive asynchronous tasks.'
    #[test]
    fn signer_in_async_context() -> Result<()> {
        async fn async_context() -> Result<()> {
            let ctx = match gnupg::Context::ephemeral() {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("Failed to create ephemeral context: {}", e);
                    eprintln!("Most likely, GnuPG isn't installed.");
                    eprintln!("Skipping test.");
                    return Ok(());
                },
            };

            let key = Cert::from_bytes(crate::tests::key("testy-new.pgp"))?
                .primary_key().key().clone();
            let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?;
            let algo = HashAlgorithm::default();
            let digest = algo.context()?.into_digest()?;
            let _ = pair.sign(algo, &digest);
            Ok(())
        }

        let rt = tokio::runtime::Runtime::new()?;
        rt.block_on(async_context())
    }

    /// Asserts that a <KeyPair as Decryptor> is usable from an async
    /// context.
    ///
    /// Previously, the test died with
    ///
    ///     thread 'gnupg::tests::decryptor_in_async_context' panicked
    ///     at 'Cannot start a runtime from within a runtime. This
    ///     happens because a function (like `block_on`) attempted to
    ///     block the current thread while the thread is being used to
    ///     drive asynchronous tasks.'
    #[test]
    fn decryptor_in_async_context() -> Result<()> {
        async fn async_context() -> Result<()> {
            let ctx = match gnupg::Context::ephemeral() {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("Failed to create ephemeral context: {}", e);
                    eprintln!("Most likely, GnuPG isn't installed.");
                    eprintln!("Skipping test.");
                    return Ok(());
                },
            };

            let key = Cert::from_bytes(crate::tests::key("testy-new.pgp"))?
                .keys().nth(1).unwrap().key().clone();
            let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?;
            let ciphertext = Ciphertext::ECDH {
                e: vec![].into(),
                key: vec![].into_boxed_slice(),
            };
            let _ = pair.decrypt(&ciphertext, None);
            Ok(())
        }

        let rt = tokio::runtime::Runtime::new()?;
        rt.block_on(async_context())
    }

    // Disabled on Windows.  Re-enable when this issue is fixed:
    //
    // https://gitlab.com/sequoia-pgp/sequoia/-/issues/1093
    //
    // SEE OTHER INSTANCES OF THIS ABOVE (search for comment).
    #[cfg(not(windows))]
    #[tokio::test]
    async fn non_existent_home_directory() -> Result<()> {
        let tempdir = tempfile::tempdir()?;
        let homedir = tempdir.path().join("foo");

        let result = Agent::connect_to(&homedir).await;
        match result {
            Ok(_agent) => panic!("Created an agent for a non-existent home!"),
            Err(err) => {
                if let Error::GnuPGHomeMissing(_) = err {
                    // Correct error.
                } else {
                    panic!("Expected Error::GnuPGHomeMissing, got: {}", err);
                }
            }
        }

        Ok(())
    }

    // Test that we can import a key and sign a message.
    #[tokio::test]
    async fn import_key_and_sign() -> Result<()> {
        let ctx = match gnupg::Context::ephemeral() {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Failed to create ephemeral context: {}", e);
                eprintln!("Most likely, GnuPG isn't installed.");
                eprintln!("Skipping test.");
                return Ok(());
            },
        };

        let mut agent = Agent::connect(&ctx).await?;

        let testy = import_testy_new(&mut agent).await?;

        let key = testy.primary_key().key();
        let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?;
        let algo = HashAlgorithm::default();
        let digest = algo.context()?.into_digest()?;
        if let Err(err) = pair.sign(algo, &digest) {
            panic!("Signing: {}", err);
        }
        Ok(())
    }

    // Test that we can list imported keys.
    #[tokio::test]
    async fn list_keys() -> Result<()> {
        use std::collections::HashSet;

        let ctx = match gnupg::Context::ephemeral() {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Failed to create ephemeral context: {}", e);
                eprintln!("Most likely, GnuPG isn't installed.");
                eprintln!("Skipping test.");
                return Ok(());
            },
        };

        let mut agent = Agent::connect(&ctx).await?;

        let keys = agent.list_keys().await.expect("list keys works");
        assert_eq!(keys.len(), 0);

        let testy = import_testy_new(&mut agent).await?;
        let testy_keygrips = testy.keys().into_iter()
            .map(|ka| Keygrip::of(ka.key().mpis()))
            .collect::<Result<HashSet<Keygrip>, _>>()
            .expect("can compute keygrip");
        assert!(testy_keygrips.len() > 0);

        let keys = agent.list_keys().await.expect("list keys works");
        let keys = keys.into_iter()
            .map(|k| k.keygrip().clone())
            .collect::<HashSet<Keygrip>>();

        assert_eq!(testy_keygrips, keys);

        Ok(())
    }

    // Test Agent::key_info.
    #[tokio::test]
    async fn key_info() -> Result<()> {
        use std::collections::HashSet;

        let ctx = match gnupg::Context::ephemeral() {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Failed to create ephemeral context: {}", e);
                eprintln!("Most likely, GnuPG isn't installed.");
                eprintln!("Skipping test.");
                return Ok(());
            },
        };

        let mut agent = Agent::connect(&ctx).await?;

        let testy = import_testy_new(&mut agent).await?;
        let testy_keygrips = testy.keys().into_iter()
            .map(|ka| Keygrip::of(ka.key().mpis()))
            .collect::<Result<HashSet<Keygrip>, _>>()
            .expect("can compute keygrip");
        assert!(testy_keygrips.len() > 0);

        for ka in testy.keys() {
            let keygrip = Keygrip::of(ka.key().mpis()).expect("has a keygrip");
            match agent.key_info(&keygrip).await {
                Ok(info) => {
                    assert_eq!(info.keygrip(), &keygrip);
                }
                Err(err) => {
                    panic!("Getting keyinfo for {}: {}", keygrip, err);
                }
            }
        }

        Ok(())
    }

    // Test Agent::preset_passphrase
    //
    // Disabled on Windows.  Re-enable when this issue is fixed:
    //
    // https://gitlab.com/sequoia-pgp/sequoia/-/issues/1093
    //
    // SEE OTHER INSTANCES OF THIS ABOVE (search for comment).
    #[cfg(not(windows))]
    #[tokio::test]
    async fn preset_passphrase() -> Result<()> {
        trace(true);
        tracer!(TRACE, "preset_passphrase");

        async fn test(file: &str, password: &str)
            -> Result<()>
        {
            let ctx = match gnupg::Context::ephemeral() {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("Failed to create ephemeral context: {}", e);
                    eprintln!("Most likely, GnuPG isn't installed.");
                    eprintln!("Skipping test.");
                    return Ok(());
                },
            };

            let mut agent = Agent::connect(&ctx).await?;

            let cert = Cert::from_bytes(crate::tests::key(file))?;
            import_keys(&mut agent, &cert).await?;
            let vc = cert.with_policy(P, None).expect("valid cert");
            let key = vc.keys().for_signing().next().expect("have signing key");
            let key_keygrip = Keygrip::of(key.mpis()).expect("has a keygrip");

            t!("Testing with {} ({})", key.fingerprint(), key_keygrip);
            t!("gpg knows about:");
            let mut saw = false;
            for (i, info) in agent.list_keys().await.expect("works").iter().enumerate() {
                t!("{}. {}: {:?}, password cached: {}",
                   i + 1, info.keygrip(), info.protection(),
                   info.passphrase_cached());
                if info.keygrip() == &key_keygrip {
                    saw = true;
                }
            }
            assert!(saw, "Failed to load test key");

            // Try with allow-preset-passphrase not set and an
            // incorrect password.  This should fail.
            let r = agent.preset_passphrase(&key_keygrip, "wrong".into()).await;
            match r {
                Ok(()) => panic!("preset_passphrase should fail if \
                                  'allow-preset-passphrase' is not set."),
                Err(err) => {
                    if let Error::Assuan(assuan::Error::OperationFailed(_)) = err {
                        // That's what we expect.
                    } else {
                        panic!("Expected assuan::Error::OperationFailed, \
                                but got {}", err);
                    }
                }
            }

            // Set allow-preset-passphrase.
            let dot_gnupg = ctx.homedir().expect("have a homedir");
            let conf = dot_gnupg.join("gpg-agent.conf");
            let mut f = File::options().create(true).append(true).open(&conf)
                .with_context(|| {
                    format!("Opening {}", conf.display())
                }).expect("can open gpg-agent.conf");
            writeln!(&mut f, "allow-preset-passphrase").expect("can write");
            drop(f);

            agent.reload().await.expect("valid");

            // Try with allow-preset-passphrase set, but still an
            // incorrect password.  This should still fail.
            let r = agent.preset_passphrase(
                &key_keygrip, "this is the wrong passphrase".into()).await;
            match r {
                Ok(()) => (),
                Err(err) => {
                    panic!("Failed to set password: {}", err);
                }
            }

            let algo = HashAlgorithm::default();
            let digest = algo.context()?.into_digest()?;
            let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?
                // Don't prompt for the passphrase.
                .suppress_pinentry();
            match pair.sign(algo, &digest) {
                Ok(_) => {
                    panic!("Signing should have failed");
                }
                Err(err) => {
                    t!("Signing failed (expected): {}", err);
                }
            }

            // Finally, try to sign a message using the correct
            // passphrase.
            let r = agent.preset_passphrase(&key_keygrip, password.into()).await;
            match r {
                Ok(()) => (),
                Err(err) => {
                    panic!("Failed to set password: {}", err);
                }
            }

            t!("gpg-agent's key info:");
            for (i, info) in agent.list_keys().await.unwrap().iter().enumerate()
            {
                t!("  {}. {}: {:?}, password cached: {}",
                   i + 1, info.keygrip(), info.protection(),
                   info.passphrase_cached());
            }

            let algo = HashAlgorithm::default();
            let digest = algo.context()?.into_digest()?;
            let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?
                // Don't prompt for the passphrase.
                .suppress_pinentry();
            match pair.sign(algo, &digest) {
                Ok(_) => {
                }
                Err(err) => {
                    panic!("Signing failed (unexpected): {}", err);
                }
            }

            // Now, forget the passphrase and try to use the key.  It
            // should fail again.
            agent.forget_passphrase(&key_keygrip.to_string(), |_| ()).await
                .expect("can forget");

            let algo = HashAlgorithm::default();
            let digest = algo.context()?.into_digest()?;
            let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?
                // Don't prompt for the passphrase.
                .suppress_pinentry();
            match pair.sign(algo, &digest) {
                Ok(_) => {
                    panic!("Signing should have failed");
                }
                Err(err) => {
                    t!("Signing failed (expected): {}", err);
                }
            }

            Ok(())
        }

        test("password-xyzzy-private.pgp", "xyzzy").await.expect("all okay");
        test("password-foospacespacebar-private.pgp", "foo  bar").await.expect("all okay");

        Ok(())
    }

    // Test Agent::export
    #[tokio::test]
    async fn export() -> Result<()> {
        use std::collections::HashSet;

        let ctx = match gnupg::Context::ephemeral() {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Failed to create ephemeral context: {}", e);
                eprintln!("Most likely, GnuPG isn't installed.");
                eprintln!("Skipping test.");
                return Ok(());
            },
        };

        let mut agent = Agent::connect(&ctx).await?;

        let testy = import_testy_new(&mut agent).await?;
        let testy_keygrips = testy.keys().into_iter()
            .map(|ka| Keygrip::of(ka.key().mpis()))
            .collect::<Result<HashSet<Keygrip>, _>>()
            .expect("can compute keygrip");
        assert!(testy_keygrips.len() > 0);

        for ka in testy.keys().secret() {
            let keygrip = Keygrip::of(ka.key().mpis()).expect("has a keygrip");
            match agent.export(ka.key().parts_as_public().clone()).await {
                Ok(key) => {
                    assert_eq!(ka.key(), &key);
                }
                Err(err) => {
                    panic!("Exporting key {}: {}", keygrip, err);
                }
            }
        }

        Ok(())
    }
}
