KeyBoxen 0.1.0

Standalone secret-service daemon for window managers
// Copyright (C) 2022 KeyBoxen Authors
// SPDX-License-Identifier: GPL-3.0-or-later

/// Controller task
///
/// This task coordinates the communication with pinentry. It takes
/// care of the timing and sequencing of commands w.r.t replies.
use super::{commands, responses};
use super::{Command, DialogParams, DialogType, ErrorKind, Response, Result};
use std::process::Stdio;
use tokio::process;
use tokio::sync::mpsc;
use tokio::task;

/// User prompt
///
/// This function initiates the process of prompting the user for a
/// required input.
///
/// # Arguments
/// * `params` : [`DialogParams`] instance that contains all necessary
/// information for starting the prompt
pub async fn prompt(params: DialogParams) -> Result<Option<Vec<u8>>> {
    // Start the program and associated tasks
    let mut dialog = Dialog::start().await?;
    // Wait for the first OK from pinentry
    dialog.ack().await?;
    // Setup all dialog parameters and show it to user
    dialog.show(params).await?;
    // Wait here till we get OK or ERR with or without data
    let outcome = dialog.outcome().await;
    // Stop the pinentry program, tasks and channels
    dialog.exit().await?;
    // Return the result
    outcome
}

/// Interface to live pinentry instance
pub struct Dialog {
    program: process::Child,
    commands: mpsc::Sender<Command>,
    responses: mpsc::Receiver<Response>,
}

impl Dialog {
    /// Start program and communication tasks
    async fn start() -> Result<Self> {
        // Start the program
        let mut program = process::Command::new("/usr/bin/pinentry")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .kill_on_drop(true)
            .spawn()?;

        // Commanding task initialization
        let stdin = program.stdin.take().unwrap();
        let (cmdtx, cmdrx) = mpsc::channel::<Command>(1);
        task::spawn(commands::transmit(stdin, cmdrx));

        // Response task initialization
        let stdout = program.stdout.take().unwrap();
        let (restx, resrx) = mpsc::channel::<Response>(1);
        task::spawn(responses::listen(stdout, restx));

        Ok(Self {
            program,
            commands: cmdtx,
            responses: resrx,
        })
    }

    /// Setup dialog parameters for the running instance and show it
    async fn show(&mut self, params: DialogParams) -> Result<()> {
        self.send(Command::Title(params.title)).await?;
        self.ack().await?;

        self.send(Command::Description(params.description)).await?;
        self.ack().await?;

        self.send(Command::Prompt(params.prompt)).await?;
        self.ack().await?;

        self.send(Command::OKButton(params.label_yes)).await?;
        self.ack().await?;

        self.send(Command::CancelButton(params.label_cancel))
            .await?;
        self.ack().await?;

        if let Some(label) = params.label_no {
            self.send(Command::NotOKButton(label)).await?;
            self.ack().await?;
        }

        // Show the appropriate dialog
        match params.dialog_type {
            DialogType::Password => self.send(Command::Password),
            DialogType::Confirmation => self.send(Command::Confirm),
            DialogType::Notification => self.send(Command::Notify),
        }
        .await?;

        Ok(())
    }

    /// Send a command to pinentry
    async fn send(&mut self, cmd: Command) -> Result<()> {
        Ok(self.commands.send(cmd).await?)
    }

    /// Receive a response from pinentry
    async fn get(&mut self) -> Result<Response> {
        Ok(self
            .responses
            .recv()
            .await
            .ok_or(ErrorKind::EmptyResponse)?)
    }

    /// Wait for acknowledgement
    async fn ack(&mut self) -> Result<()> {
        match self.get().await? {
            Response::Success(..) => Ok(()),
            Response::Failure(..) => Err(ErrorKind::CommandFail),
            _ => Err(ErrorKind::UnexpectedResponse(format!(
                "Expected acknowledgement"
            ))),
        }
    }

    /// Get outcome of the dialog
    ///
    /// The task will wait here till an OK or ERR is received with or
    /// without data
    async fn outcome(&mut self) -> Result<Option<Vec<u8>>> {
        let out;
        loop {
            out = match self.get().await? {
                Response::Success(..) => Ok(None),
                Response::Failure(83886179, ..) => Err(ErrorKind::UserCancel),
                Response::Failure(83886194, ..) => Err(ErrorKind::UserNo),
                Response::DataOK(data, ..) => Ok(Some(data)),
                Response::DataFail(_, 83886179, ..) => Err(ErrorKind::UserCancel),
                Response::DataFail(_, 83886194, ..) => Err(ErrorKind::UserNo),
                _ => continue,
            };
            break out;
        }
    }

    /// Stop the pinentry program, tasks and channels
    async fn exit(mut self) -> Result<()> {
        self.send(Command::Exit).await?;
        self.ack().await?;
        self.program.start_kill()?;
        drop(self.commands);
        drop(self.responses);
        Ok(())
    }
}