use std::io::{self, Error, ErrorKind, Read};
use std::os::fd::{AsRawFd, RawFd};
use std::{fs, mem};
use libc::{ECHO, ECHONL, TCSANOW, tcsetattr, termios};
use super::cutils::cerr;
use super::securemem::PamBuffer;
pub struct HiddenInput {
tty: fs::File,
term_orig: termios,
}
impl HiddenInput {
#[cfg_attr(tarpaulin, ignore)]
fn new() -> io::Result<Option<Self>> {
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, &raw const term) })?;
Ok(Some(Self { tty, term_orig }))
}
}
impl Drop for HiddenInput {
#[cfg_attr(tarpaulin, ignore)]
fn drop(&mut self) {
unsafe {
libc::tcsetattr(
self.tty.as_raw_fd(),
libc::TCSAFLUSH,
&raw const self.term_orig,
);
}
}
}
#[cfg_attr(tarpaulin, ignore)]
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() })
}
const EOL: u8 = 0x0A;
fn read_unbuffered(source: &mut impl io::Read) -> io::Result<PamBuffer> {
let mut password = PamBuffer::default();
let mut pwd_iter = password.iter_mut();
#[allow(clippy::unbuffered_bytes)] 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<'_> {
#[cfg_attr(tarpaulin, ignore)]
pub fn open_tty() -> io::Result<Self> {
Ok(Terminal::Tty(
fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")?,
))
}
pub fn open_stdie() -> Terminal<'static> {
Terminal::StdIE(io::stdin().lock(), io::stderr().lock())
}
#[cfg_attr(tarpaulin, ignore)]
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: &impl AsRef<str>) -> io::Result<()> {
write_unbuffered(&mut self.sink(), text.as_ref())
}
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::{Terminal, read_unbuffered, write_unbuffered};
use std::io::{self, Read, Write};
#[test]
#[allow(clippy::string_lit_as_bytes)]
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");
}
struct MockFile {
read_data: Vec<u8>,
read_pos: usize,
write_data: Vec<u8>,
}
impl MockFile {
fn new(data: &str) -> Self {
Self {
read_data: data.as_bytes().to_vec(),
read_pos: 0,
write_data: Vec::new(),
}
}
}
impl Read for MockFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let available = self.read_data.len().saturating_sub(self.read_pos);
let to_read = buf.len().min(available);
if to_read > 0 {
buf[..to_read]
.copy_from_slice(&self.read_data[self.read_pos..self.read_pos + to_read]);
self.read_pos += to_read;
}
Ok(to_read)
}
}
impl Write for MockFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.write_data.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn test_terminal_variant_matching() {
let terminal = Terminal::open_stdie();
match terminal {
Terminal::StdIE(_, _) => {}
Terminal::Tty(_) => {
panic!("Expected StdIE variant, got Tty");
}
}
}
}