diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2aa6ebba0..88744f309 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -43,10 +43,15 @@ jobs: - name: Build client (tokio / OpenSSL) run: | cargo build --manifest-path ./client/Cargo.toml --no-default-features --features tokio --features openssl_crypto + - name: Build CLI run: | cargo build --manifest-path ./cli/Cargo.toml + - name: Build Portal + run: | + cargo build --manifest-path ./portal/Cargo.toml + - name: Test (native) run: | cargo test --manifest-path ./client/Cargo.toml --no-default-features --features async-std --features native_crypto diff --git a/.gitignore b/.gitignore index 99ba9518c..c1a2ebf67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target +/portal/target Cargo.lock cli/target diff --git a/Cargo.toml b/Cargo.toml index 07228e27f..32a0845f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ resolver = "2" members = [ "client", "cli", + "portal", ] diff --git a/portal/Cargo.toml b/portal/Cargo.toml new file mode 100644 index 000000000..6564ac13b --- /dev/null +++ b/portal/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "oo7-portal" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures-channel = "0.3" +futures-util = "0.3" +oo7 = { path = "../client" } +ring = "0.17.5" +secrecy = { version = "0.8", features = ["alloc"] } +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.17", features = ["io-util", "net", "macros", "rt-multi-thread"] } +tracing = "0.1" +tracing-subscriber = "0.3" +zbus = "3.5.0" +zeroize = { version = "1", features = ["zeroize_derive"] } diff --git a/portal/data/oo7-portal.portal b/portal/data/oo7-portal.portal new file mode 100644 index 000000000..761808852 --- /dev/null +++ b/portal/data/oo7-portal.portal @@ -0,0 +1,4 @@ +[portal] +DBusName=org.freedesktop.impl.portal.desktop.oo7 +Interfaces=org.freedesktop.impl.portal.Secret +UseIn=gnome diff --git a/portal/data/oo7-portal.service b/portal/data/oo7-portal.service new file mode 100644 index 000000000..5672bdd5f --- /dev/null +++ b/portal/data/oo7-portal.service @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.impl.portal.Secret +Exec=@bindir@/oo7-portal diff --git a/portal/src/error.rs b/portal/src/error.rs new file mode 100644 index 000000000..c318971fc --- /dev/null +++ b/portal/src/error.rs @@ -0,0 +1,35 @@ +use oo7::zbus; + +#[derive(zbus::DBusError, Debug)] +pub enum Error { + Owned(String), +} + +impl From for Error { + fn from(err: zbus::fdo::Error) -> Self { + Self::Owned(err.to_string()) + } +} +impl From for Error { + fn from(err: zbus::Error) -> Self { + Self::Owned(err.to_string()) + } +} + +impl From for Error { + fn from(err: oo7::dbus::Error) -> Self { + Self::Owned(err.to_string()) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Self::Owned(err.to_string()) + } +} + +impl From for Error { + fn from(err: ring::error::Unspecified) -> Self { + Self::Owned(err.to_string()) + } +} diff --git a/portal/src/main.rs b/portal/src/main.rs new file mode 100644 index 000000000..e0ea21cd1 --- /dev/null +++ b/portal/src/main.rs @@ -0,0 +1,153 @@ +mod error; +mod request; + +use std::{ + collections::HashMap, + future::pending, + os::{ + fd::{AsRawFd, FromRawFd}, + unix::net::UnixStream, + }, +}; + +use futures_util::FutureExt; +use oo7::{ + dbus::Service, + zbus::{self, dbus_interface, zvariant}, +}; +use ring::rand::SecureRandom; +use tokio::io::AsyncWriteExt; +use zvariant::OwnedObjectPath; + +use crate::{ + error::Error, + request::{Request, ResponseType}, +}; + +const PORTAL_VERSION: u32 = 1; +const PORTAL_SECRET_SIZE: usize = 64; +const PORTAL_NAME: &str = "org.freedesktop.impl.portal.desktop.oo7"; +const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop"; + +struct Secret; + +#[dbus_interface(name = "org.freedesktop.impl.portal.Secret")] +impl Secret { + #[dbus_interface(property, name = "version")] + fn version(&self) -> u32 { + PORTAL_VERSION + } + + #[dbus_interface(out_args("response", "results"))] + async fn retrieve_secret( + &self, + #[zbus(object_server)] object_server: &zbus::ObjectServer, + handle: OwnedObjectPath, + app_id: &str, + fd: zvariant::Fd, + options: HashMap<&str, zvariant::Value<'_>>, + ) -> Result<(ResponseType, HashMap<&str, zvariant::OwnedValue>), Error> { + tracing::debug!("Request from app: {app_id} with options: {options:?}"); + + let (sender, receiver) = futures_channel::oneshot::channel(); + let request = Request::new(&handle, sender); + object_server.at(&handle, request).await?; + + let fut_1 = async move { + let res = match send_secret_to_app(app_id, fd).await { + Ok(_) => ResponseType::Success, + Err(err) => { + tracing::error!("could not retrieve secret: {err}"); + ResponseType::Other + } + }; + + // We do not accept Close request anymore here. + tracing::debug!("Request {handle} handled"); + object_server.remove::(&handle).await.unwrap(); + + Ok((res, HashMap::new())) + }; + + let fut_2 = async move { + receiver.await.unwrap(); + Ok((ResponseType::Cancelled, HashMap::new())) + }; + + let t1 = fut_1.fuse(); + let t2 = fut_2.fuse(); + + futures_util::pin_mut!(t1, t2); + + futures_util::select! { + fut_1_res = t1 => fut_1_res, + fut_2_res = t2 => fut_2_res, + } + } +} + +fn generate_secret() -> Result>, Error> { + let mut secret = [0; PORTAL_SECRET_SIZE]; + let rand = ring::rand::SystemRandom::new(); + rand.fill(&mut secret)?; + Ok(zeroize::Zeroizing::new(secret.to_vec())) +} + +/// Generates, stores and send the secret back to the fd stream +async fn send_secret_to_app(app_id: &str, fd: zvariant::Fd) -> Result<(), Error> { + let service = Service::new().await?; + let collection = match service.default_collection().await { + Err(oo7::dbus::Error::NotFound(_)) => { + service + .create_collection("Default", Some(oo7::dbus::DEFAULT_COLLECTION)) + .await + } + e => e, + }?; + let attributes = HashMap::from([("app_id", app_id)]); + + let secret = if let Some(item) = collection.search_items(&attributes).await?.first() { + item.secret().await? + } else { + tracing::debug!("Could not find secret for {app_id}, creating one"); + let secret = generate_secret()?; + collection + .create_item( + &format!("Secret Portal token for {app_id}"), + &attributes, + &secret, + true, + // TODO Find a better one. + "text/plain", + ) + .await?; + + secret + }; + + // Write the secret to the FD. + let raw_fd = fd.as_raw_fd(); + let mut stream = unsafe { tokio::net::UnixStream::from_std(UnixStream::from_raw_fd(raw_fd)) }?; + stream.write_all(&secret).await?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), zbus::Error> { + tracing_subscriber::fmt::init(); + + let backend = Secret; + let cnx = zbus::ConnectionBuilder::session()? + .serve_at(PORTAL_PATH, backend)? + .build() + .await?; + // NOTE For debugging. + let flags = zbus::fdo::RequestNameFlags::ReplaceExisting + | zbus::fdo::RequestNameFlags::AllowReplacement; + cnx.request_name_with_flags(PORTAL_NAME, flags).await?; + + loop { + pending::<()>().await; + } +} diff --git a/portal/src/request.rs b/portal/src/request.rs new file mode 100644 index 000000000..a1a56d703 --- /dev/null +++ b/portal/src/request.rs @@ -0,0 +1,52 @@ +use std::sync::Mutex; + +use oo7::zbus::{ + self, dbus_interface, + zvariant::{self, ObjectPath, Type}, +}; +use serde::Serialize; + +#[derive(Serialize, PartialEq, Eq, Debug, Type)] +pub enum ResponseType { + Success = 0, + Cancelled = 1, + Other = 2, +} + +pub struct Request { + handle_path: zvariant::ObjectPath<'static>, + sender: Mutex>>, +} + +impl Request { + pub fn new( + handle_path: &ObjectPath<'static>, + sender: futures_channel::oneshot::Sender<()>, + ) -> Self { + tracing::debug!("Request `{:?}` exported", handle_path.as_str()); + Self { + handle_path: handle_path.clone(), + sender: Mutex::new(Some(sender)), + } + } +} + +#[dbus_interface(name = "org.freedesktop.impl.portal.Request")] +impl Request { + async fn close( + &self, + #[zbus(object_server)] server: &zbus::ObjectServer, + ) -> zbus::fdo::Result<()> { + tracing::debug!("Request `{}` closed", self.handle_path); + server.remove::(&self.handle_path).await?; + + if let Ok(mut guard) = self.sender.lock() { + if let Some(sender) = (*guard).take() { + // This will Err out if the receiver has been dropped. + let _ = sender.send(()); + } + } + + Ok(()) + } +}