use std::io::{self, Error, ErrorKind, Read};
use std::os::fd::{AsRawFd, RawFd};
use std::{fs, mem};
use libc::{tcsetattr, termios, ECHO, ECHONL, TCSANOW};
use crate::cutils::cerr;
use super::securemem::PamBuffer;
pub struct HiddenInput {
tty: fs::File,
term_orig: termios,
}
impl HiddenInput {
fn new() -> io::Result<Option<HiddenInput>> {
let Ok(tty) = fs::File::open("/dev/tty") else {
return Ok(None);
};
let fd = tty.as_raw_fd();
let mut term = safe_tcgetattr(fd)?;
let term_orig = safe_tcgetattr(fd)?;
term.c_lflag &= !ECHO;
term.c_lflag |= ECHONL;
cerr(unsafe { tcsetattr(fd, TCSANOW, &term) })?;
Ok(Some(HiddenInput { tty, term_orig }))
}
}
impl Drop for HiddenInput {
fn drop(&mut self) {
unsafe {
tcsetattr(self.tty.as_raw_fd(), TCSANOW, &self.term_orig);
}
}
}
fn safe_tcgetattr(fd: RawFd) -> io::Result<termios> {
let mut term = mem::MaybeUninit::<termios>::uninit();
cerr(unsafe { ::libc::tcgetattr(fd, term.as_mut_ptr()) })?;
Ok(unsafe { term.assume_init() })
}
fn read_unbuffered(source: &mut impl io::Read) -> io::Result<PamBuffer> {
let mut password = PamBuffer::default();
let mut pwd_iter = password.iter_mut();
const EOL: u8 = 0x0A;
let input = source.bytes().take_while(|x| x.as_ref().ok() != Some(&EOL));
for read_byte in input {
if let Some(dest) = pwd_iter.next() {
*dest = read_byte?
} else {
return Err(Error::new(
ErrorKind::OutOfMemory,
"incorrect password attempt",
));
}
}
Ok(password)
}
fn write_unbuffered(sink: &mut impl io::Write, text: &str) -> io::Result<()> {
sink.write_all(text.as_bytes())?;
sink.flush()
}
pub enum Terminal<'a> {
Tty(fs::File),
StdIE(io::StdinLock<'a>, io::StderrLock<'a>),
}
impl Terminal<'_> {
pub fn open_tty() -> io::Result<Self> {
Ok(Terminal::Tty(
fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")?,
))
}
pub fn open_stdie() -> io::Result<Self> {
Ok(Terminal::StdIE(io::stdin().lock(), io::stderr().lock()))
}
pub fn read_password(&mut self) -> io::Result<PamBuffer> {
let mut input = self.source();
let _hide_input = HiddenInput::new()?;
read_unbuffered(&mut input)
}
pub fn read_cleartext(&mut self) -> io::Result<PamBuffer> {
read_unbuffered(&mut self.source())
}
pub fn prompt(&mut self, text: &str) -> io::Result<()> {
write_unbuffered(&mut self.sink(), text)
}
fn source(&mut self) -> &mut dyn io::Read {
match self {
Terminal::StdIE(x, _) => x,
Terminal::Tty(x) => x,
}
}
fn sink(&mut self) -> &mut dyn io::Write {
match self {
Terminal::StdIE(_, x) => x,
Terminal::Tty(x) => x,
}
}
}
#[cfg(test)]
mod test {
use super::{read_unbuffered, write_unbuffered};
#[test]
fn miri_test_read() {
let mut data = "password123\nhello world".as_bytes();
let buf = read_unbuffered(&mut data).unwrap();
assert_eq!(
buf.iter()
.map(|&b| b as char)
.take_while(|&x| x != '\0')
.collect::<String>(),
"password123"
);
assert_eq!(std::str::from_utf8(data).unwrap(), "hello world");
}
#[test]
fn miri_test_longpwd() {
assert!(read_unbuffered(&mut "a".repeat(511).as_bytes()).is_ok());
assert!(read_unbuffered(&mut "a".repeat(512).as_bytes()).is_err());
}
#[test]
fn miri_test_write() {
let mut data = Vec::new();
write_unbuffered(&mut data, "prompt").unwrap();
assert_eq!(std::str::from_utf8(&data).unwrap(), "prompt");
}
}