use std::{
fs::File,
io::BufReader,
os::fd::{AsRawFd, FromRawFd, RawFd},
str::FromStr,
};
pub mod error;
use anyhow::anyhow;
use error::Error;
use libassuan::{
handler::{assuan_process, assuan_process_done, assuan_write_line, AssuanProcessResult},
AssuanContext,
};
pub(crate) fn unenc(val: &str) -> Result<String, Error> {
urlencoding::decode(val)
.map(|v| v.to_string())
.map_err(|err| anyhow!("Fail urlencoding::decode. {err}"))
}
const PE_CMD_SETDESC: &str = "SETDESC";
const PE_CMD_PROMPT: &str = "SETPROMPT";
const PE_CMD_KEYINFO: &str = "SETKEYINFO";
const PE_CMD_REPEAT: &str = "SETREPEAT";
const PE_CMD_SETOK: &str = "SETOK";
const PE_CMD_SETNOTOK: &str = "SETNOTOK";
const PE_CMD_SETCANCEL: &str = "SETCANCEL";
const PE_CMD_GETPIN: &str = "GETPIN";
const PE_CMD_SETTITLE: &str = "SETTITLE";
const PE_CMD_SETERROR: &str = "SETERROR";
const PINENTRY_COMMANDS: [&str; 10] = [
PE_CMD_SETDESC,
PE_CMD_PROMPT,
PE_CMD_KEYINFO,
PE_CMD_REPEAT,
PE_CMD_SETOK,
PE_CMD_SETNOTOK,
PE_CMD_SETCANCEL,
PE_CMD_GETPIN,
PE_CMD_SETTITLE,
PE_CMD_SETERROR,
];
#[derive(Debug, PartialEq, Eq)]
enum PinentryCmd {
SetDesc,
Prompt,
KeyInfo,
Repeat,
SetOk,
SetNotOk,
SetCancel,
GetPin,
SetTitle,
SetError,
}
impl FromStr for PinentryCmd {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
PE_CMD_SETDESC => Ok(Self::SetDesc),
PE_CMD_PROMPT => Ok(Self::Prompt),
PE_CMD_KEYINFO => Ok(Self::KeyInfo),
PE_CMD_REPEAT => Ok(Self::Repeat),
PE_CMD_SETOK => Ok(Self::SetOk),
PE_CMD_SETNOTOK => Ok(Self::SetNotOk),
PE_CMD_SETCANCEL => Ok(Self::SetCancel),
PE_CMD_GETPIN => Ok(Self::GetPin),
PE_CMD_SETTITLE => Ok(Self::SetTitle),
PE_CMD_SETERROR => Ok(Self::SetError),
_ => Err(anyhow!("{s} not valid PinentryCmd")),
}
}
}
const PE_OPT_TTYNAME: &str = "ttyname";
const PINENTRY_OPTIONS: [&str; 1] = [PE_OPT_TTYNAME];
#[derive(Debug, PartialEq, Eq)]
enum PinentryOption {
TtyName,
}
impl FromStr for PinentryOption {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
PE_OPT_TTYNAME => Ok(Self::TtyName),
_ => Err(anyhow!("{s} not valid PinentryOption")),
}
}
}
#[derive(Debug, Default)]
pub struct Pinentry {
pub title: Option<String>,
pub description: Option<String>,
pub error: Option<String>,
pub prompt: Option<String>,
pub ok: Option<String>,
pub notok: Option<String>,
pub cancel: Option<String>,
pub ttyname: Option<String>,
}
pub trait PinentryResolver {
fn get_pin(&mut self, pinentry: &mut Pinentry) -> Result<String, Error>;
}
impl Pinentry {
pub fn get_resolver_fd_in_out(&self) -> Result<(File, File), Error> {
let read = std::fs::OpenOptions::new()
.read(true)
.open(self.ttyname.clone().unwrap_or("/dev/stdin".to_string()))
.map_err(|err| anyhow!("Fail open read resolver_fd_in_out: {err}"))?;
let write = std::fs::OpenOptions::new()
.write(true)
.open(self.ttyname.clone().unwrap_or("/dev/stdout".to_string()))
.map_err(|err| anyhow!("Fail open write resolver_fd_in_out: {err}"))?;
Ok((read, write))
}
pub fn run_loop(
mut self,
fd_in: RawFd,
fd_out: RawFd,
mut resolver: impl PinentryResolver,
) -> Result<(), Error> {
let mut ctx = AssuanContext::new(fd_in, fd_out)
.register_commands(&PINENTRY_COMMANDS)
.register_options(&PINENTRY_OPTIONS);
assuan_write_line(&mut ctx, "OK pleased to meet you.").unwrap();
loop {
match assuan_process(&mut ctx) {
Ok(res) => match res {
AssuanProcessResult::Command(cmd_str, line) => {
let Ok(cmd) = PinentryCmd::from_str(&cmd_str) else {
assuan_process_done(&mut ctx);
continue;
};
let line = unenc(&line)?;
match cmd {
PinentryCmd::SetDesc => {
self.description = Some(line);
}
PinentryCmd::Prompt => {
self.prompt = Some(line);
}
PinentryCmd::SetOk => {
self.ok = Some(line);
}
PinentryCmd::SetNotOk => {
self.notok = Some(line);
}
PinentryCmd::SetCancel => {
self.cancel = Some(line);
}
PinentryCmd::GetPin => {
match resolver.get_pin(&mut self) {
Ok(password) => {
assuan_write_line(&mut ctx, &format!("D {password}"))
.expect("Fail assuan write");
}
Err(err) => {
panic!("{err}");
}
};
}
PinentryCmd::SetTitle => {
self.title = Some(line);
}
PinentryCmd::SetError => {
self.error = Some(line);
}
_ => {
}
}
assuan_process_done(&mut ctx);
}
AssuanProcessResult::Option(key, value) => {
let Ok(option) = PinentryOption::from_str(&key) else {
continue;
};
match option {
PinentryOption::TtyName => {
self.ttyname = Some(value);
}
}
}
_ => {}
},
Err(err) => {
panic!("{err}")
}
}
}
}
}