oo7 0.1.2

James Bond went on a new mission and this time as a Secret Service provider
Documentation
//! Implementation of the XDG secret portal.
//!
//! This is a modified copy from ASHPD.
use std::{
    collections::HashMap,
    io::Read,
    os::{
        fd::{AsFd, AsRawFd},
        unix::net::UnixStream,
    },
};

use futures_util::StreamExt;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use zbus::zvariant::{Fd, ObjectPath, OwnedValue, SerializeDict, Type};
use zeroize::{Zeroize, ZeroizeOnDrop};

use super::Error;

/// Secret retrieved from the
/// [XDG secret portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Secret).
///
/// This secret is generated by the portal once per app.
/// It is provided such that apps can store encrypted data with a key based on
/// this secret.
///
/// We use this secret to encrypt the app's [`Keyring`](crate::portal::Keyring).
#[derive(Debug, Zeroize, ZeroizeOnDrop)]
pub struct Secret {
    secret: Vec<u8>,
}

impl From<Vec<u8>> for Secret {
    fn from(secret: Vec<u8>) -> Self {
        Self { secret }
    }
}

impl std::ops::Deref for Secret {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        &self.secret
    }
}

#[derive(SerializeDict, Type, Debug)]
/// Specified options for a [`SecretProxy::retrieve_secret`] request.
#[zvariant(signature = "dict")]
struct RetrieveOptions {
    handle_token: String,
}

impl Default for RetrieveOptions {
    fn default() -> Self {
        let mut rng = thread_rng();
        let token: String = (&mut rng)
            .sample_iter(Alphanumeric)
            .take(10)
            .map(char::from)
            .collect();
        Self {
            handle_token: format!("oo7_{token}"),
        }
    }
}

#[derive(Debug)]
pub struct SecretProxy<'a>(zbus::Proxy<'a>);

impl<'a> SecretProxy<'a> {
    /// Create a new instance of [`SecretProxy`].
    pub async fn new(connection: &zbus::Connection) -> Result<SecretProxy<'a>, zbus::Error> {
        let proxy = zbus::ProxyBuilder::new_bare(connection)
            .interface("org.freedesktop.portal.Secret")?
            .path("/org/freedesktop/portal/desktop")?
            .destination("org.freedesktop.portal.Desktop")?
            .build()
            .await?;
        Ok(Self(proxy))
    }

    /// Retrieves a master secret for a sandboxed application.
    ///
    /// # Arguments
    ///
    /// * `fd` - Writable file descriptor for transporting the secret.
    #[doc(alias = "RetrieveSecret")]
    pub async fn retrieve_secret(&self, fd: &impl AsFd) -> Result<(), Error> {
        let options = RetrieveOptions::default();
        let cnx = self.0.connection();

        let unique_name = cnx.unique_name().unwrap();
        let unique_identifier = unique_name.trim_start_matches(':').replace('.', "_");
        let path = ObjectPath::try_from(format!(
            "/org/freedesktop/portal/desktop/request/{unique_identifier}/{}",
            options.handle_token
        ))
        .unwrap();

        #[cfg(feature = "tracing")]
        tracing::debug!(
            "Creating a '{}' proxy and listening for a response",
            path.as_str()
        );
        let request_proxy: zbus::Proxy = zbus::ProxyBuilder::new_bare(cnx)
            .interface("org.freedesktop.portal.Request")?
            .destination("org.freedesktop.portal.Desktop")?
            .path(path)?
            .build()
            .await?;

        let mut signal_stream = request_proxy.receive_signal("Response").await?;

        futures_util::try_join!(
            async {
                let message = signal_stream.next().await.unwrap();
                let (response, _details) = message.body::<(u32, HashMap<String, OwnedValue>)>()?;
                if response == 0 {
                    Ok(())
                } else {
                    Err(Error::CancelledPortalRequest)
                }
            },
            async {
                match self
                    .0
                    .call_method(
                        "RetrieveSecret",
                        &(Fd::from(fd.as_fd().as_raw_fd()), &options),
                    )
                    .await
                {
                    Ok(_) => Ok(()),
                    Err(zbus::Error::MethodError(_, _, _)) => Err(Error::PortalNotAvailable),
                    Err(e) => Err(e.into()),
                }?;
                Ok(())
            },
        )?;
        Ok(())
    }
}

pub async fn retrieve() -> Result<Secret, Error> {
    let connection = zbus::Connection::session().await?;
    #[cfg(feature = "tracing")]
    tracing::debug!("Retrieve service key using org.freedesktop.portal.Secrets");
    let proxy = match SecretProxy::new(&connection).await {
        Ok(proxy) => Ok(proxy),
        Err(zbus::Error::InterfaceNotFound) => Err(Error::PortalNotAvailable),
        Err(e) => Err(e.into()),
    }?;

    // FIXME Use async-std's (tokio's) UnixStream once
    // https://github.com/async-rs/async-std/pull/1036 is in.
    let (mut x1, x2) = UnixStream::pair()?;
    proxy.retrieve_secret(&x2).await?;
    drop(x2);
    let mut buf = Vec::new();
    x1.read_to_end(&mut buf)?;

    #[cfg(feature = "tracing")]
    tracing::debug!("Secret received from the portal successfully");

    Ok(Secret { secret: buf })
}