use std::{
io::Read,
os::fd::{AsFd, BorrowedFd},
};
use libc::{ECHO, ECHONL, ICANON, ISIG, VMIN, VTIME};
use nix::{
errno::Errno,
fcntl::OFlag,
sys::signal::{raise, Signal},
};
use crate::{
compat::{tcgetattr2, ResolveFlag, Termios2},
cookie::{safe_read, safe_tcsetattr},
err::err2no,
io::write_all,
lookup::safe_open_abs,
zeroize::{Zeroize, Zeroizing},
};
const DEV_TTY: &str = "/dev/tty";
const BACKSPACE: u8 = 0x08;
const DEL: u8 = 0x7F;
const CTRL_C: u8 = 0x03;
const CTRL_D: u8 = 0x04;
const CTRL_U: u8 = 0x15;
const CTRL_W: u8 = 0x17;
const ESC: u8 = 0x1B;
pub fn prompt_password(prompt: &str) -> Result<Zeroizing<Vec<u8>>, Errno> {
read_prompt(DEV_TTY, prompt)
}
pub fn read_password<R: Read>(mut reader: R) -> Result<Zeroizing<Vec<u8>>, Errno> {
read_secret(|| read_byte_io(&mut reader), || Ok(()))
}
struct RawGuard<'a> {
fd: BorrowedFd<'a>,
orig: Termios2,
}
impl<'a> RawGuard<'a> {
fn new(fd: BorrowedFd<'a>, orig: Termios2) -> Self {
RawGuard { fd, orig }
}
}
impl Drop for RawGuard<'_> {
fn drop(&mut self) {
let _ = safe_tcsetattr(self.fd, self.orig);
}
}
fn read_prompt(path: &str, prompt: &str) -> Result<Zeroizing<Vec<u8>>, Errno> {
let tty = safe_open_abs(path, OFlag::O_RDWR | OFlag::O_NOCTTY, ResolveFlag::empty())?;
write_all(tty.as_fd(), prompt.as_bytes())?;
let orig = tcgetattr2(tty.as_fd())?;
enable_raw_mode(tty.as_fd(), &orig)?;
let _guard = RawGuard::new(tty.as_fd(), orig);
read_secret(|| read_byte_fd(&tty), || write_all(tty.as_fd(), b"\n"))
}
fn enable_raw_mode(fd: BorrowedFd<'_>, orig: &Termios2) -> Result<(), Errno> {
let mut raw = *orig;
raw.c_lflag &= !(ECHO | ICANON | ECHONL | ISIG);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
safe_tcsetattr(fd, raw)
}
fn read_secret(
mut next: impl FnMut() -> Result<Option<u8>, Errno>,
mut newline: impl FnMut() -> Result<(), Errno>,
) -> Result<Zeroizing<Vec<u8>>, Errno> {
let mut password: Zeroizing<Vec<u8>> = Zeroizing::new(Vec::new());
while let Some(byte) = next()? {
match byte {
b'\n' | b'\r' => {
newline()?;
break;
}
DEL | BACKSPACE => pop_char(&mut password),
CTRL_U => password.zeroize(),
CTRL_W => clear_last_word(&mut password),
CTRL_C => {
newline()?;
raise(Signal::SIGINT)?;
return Err(Errno::EINTR);
}
CTRL_D => {
if password.is_empty() {
return Err(Errno::ENODATA);
}
}
ESC => {
let byte = match next()? {
Some(byte) => byte,
None => break,
};
if byte == b'[' || byte == b'O' {
while let Some(byte) = next()? {
if (0x40..=0x7E).contains(&byte) {
break;
}
}
}
}
byte if byte >= 0x20 => {
password.try_reserve(1).or(Err(Errno::ENOMEM))?;
password.push(byte);
}
_ => {}
}
}
Ok(password)
}
fn first_byte(nread: usize, byte: [u8; 1]) -> Option<u8> {
(nread != 0).then_some(byte[0])
}
fn read_byte_io<R: Read>(reader: &mut R) -> Result<Option<u8>, Errno> {
let mut byte = [0u8; 1];
let nread = reader.read(&mut byte).map_err(|error| err2no(&error))?;
Ok(first_byte(nread, byte))
}
fn read_byte_fd<Fd: AsFd>(fd: Fd) -> Result<Option<u8>, Errno> {
let mut byte = [0u8; 1];
let nread = safe_read(fd, &mut byte)?;
Ok(first_byte(nread, byte))
}
fn pop_char(buf: &mut Vec<u8>) {
while let Some(byte) = buf.pop() {
if byte & 0xC0 != 0x80 {
break;
}
}
}
fn clear_last_word(buf: &mut Vec<u8>) {
let trimmed = buf
.iter()
.rposition(|&byte| byte != b' ')
.map_or(0, |pos| pos.saturating_add(1));
let keep = buf[..trimmed]
.iter()
.rposition(|&byte| byte == b' ')
.map_or(0, |pos| pos.saturating_add(1));
buf[keep..].zeroize();
buf.truncate(keep);
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use std::io::{self, Read, Write};
use memchr::memmem;
use nix::{
fcntl::OFlag,
pty::{grantpt, posix_openpt, ptsname_r, unlockpt, PtyMaster},
sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal},
};
use super::*;
struct FailReader;
impl Read for FailReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::from_raw_os_error(libc::EIO))
}
}
fn pty() -> (PtyMaster, String) {
let main = posix_openpt(OFlag::O_RDWR | OFlag::O_NOCTTY).unwrap();
grantpt(&main).unwrap();
unlockpt(&main).unwrap();
let peer = ptsname_r(&main).unwrap();
(main, peer)
}
fn lflag(peer: &str) -> libc::tcflag_t {
let obs =
safe_open_abs(peer, OFlag::O_RDWR | OFlag::O_NOCTTY, ResolveFlag::empty()).unwrap();
tcgetattr2(obs.as_fd()).unwrap().c_lflag
}
#[test]
fn test_read_password_1() {
assert_eq!(&*read_password(&b"hunter2\n"[..]).unwrap(), b"hunter2");
}
#[test]
fn test_read_password_2() {
assert_eq!(
&*read_password(&b"hunter2\rignored"[..]).unwrap(),
b"hunter2"
);
}
#[test]
fn test_read_password_3() {
assert_eq!(&*read_password(&b"hunter2"[..]).unwrap(), b"hunter2");
}
#[test]
fn test_read_password_4() {
assert_eq!(&*read_password(&b""[..]).unwrap(), b"");
assert_eq!(&*read_password(&b"\n"[..]).unwrap(), b"");
}
#[test]
fn test_read_password_5() {
assert_eq!(&*read_password(&b"abX\x7fc\n"[..]).unwrap(), b"abc");
assert_eq!(&*read_password(&b"abX\x08c\n"[..]).unwrap(), b"abc");
assert_eq!(&*read_password(&b"\x7f\x08x\n"[..]).unwrap(), b"x");
}
#[test]
fn test_read_password_6() {
assert_eq!(&*read_password(&b"a\xc3\xa9\x7f\n"[..]).unwrap(), b"a");
}
#[test]
fn test_read_password_7() {
assert_eq!(
&*read_password(&b"secret\x15retry\n"[..]).unwrap(),
b"retry"
);
}
#[test]
fn test_read_password_8() {
assert_eq!(&*read_password(&b"foo bar\x17\n"[..]).unwrap(), b"foo ");
assert_eq!(&*read_password(&b"foobar\x17\n"[..]).unwrap(), b"");
assert_eq!(&*read_password(&b"foo \x17\n"[..]).unwrap(), b"");
}
#[test]
fn test_read_password_9() {
assert_eq!(read_password(&b"\x04"[..]).err(), Some(Errno::ENODATA));
}
#[test]
fn test_read_password_10() {
assert_eq!(&*read_password(&b"ab\x04cd\n"[..]).unwrap(), b"abcd");
}
#[test]
fn test_read_password_11() {
assert_eq!(&*read_password(&b"a\x1b[Db\n"[..]).unwrap(), b"ab");
assert_eq!(&*read_password(&b"a\x1bOPb\n"[..]).unwrap(), b"ab");
assert_eq!(&*read_password(&b"a\x1bZb\n"[..]).unwrap(), b"ab");
assert_eq!(&*read_password(&b"a\x1b"[..]).unwrap(), b"a");
assert_eq!(&*read_password(&b"a\x1b[1;5Db\n"[..]).unwrap(), b"ab");
}
#[test]
fn test_read_password_12() {
assert_eq!(&*read_password(&b"a\x01\x09b\n"[..]).unwrap(), b"ab");
}
#[test]
fn test_read_password_13() {
assert_eq!(
&*read_password("aé€\n".as_bytes()).unwrap(),
"aé€".as_bytes()
);
}
#[test]
fn test_read_password_14() {
assert_eq!(
&*read_password(&b"\xff\xfe\x80\n"[..]).unwrap(),
&[0xff, 0xfe, 0x80]
);
}
#[test]
fn test_read_password_15() {
assert_eq!(read_password(FailReader).err(), Some(Errno::EIO));
}
#[test]
fn test_read_password_16() {
let ignore = SigAction::new(SigHandler::SigIgn, SaFlags::empty(), SigSet::empty());
let prev = unsafe { sigaction(Signal::SIGINT, &ignore).unwrap() };
let result = read_password(&b"ab\x03cd\n"[..]);
unsafe { sigaction(Signal::SIGINT, &prev).unwrap() };
assert_eq!(result.err(), Some(Errno::EINTR));
}
#[test]
fn test_read_prompt_1() {
assert!(read_prompt("/dev/null/nonexistent", "Password: ").is_err());
}
#[test]
fn test_read_prompt_2() {
let (mut main, peer) = pty();
let before = lflag(&peer);
assert_ne!(before & (ECHO | ICANON), 0);
main.write_all(b"corr\xffct\n").unwrap();
let pass = read_prompt(&peer, "Password: ").unwrap();
assert_eq!(&*pass, b"corr\xffct");
assert_eq!(lflag(&peer), before);
let mut buf = [0u8; 64];
let n = main.read(&mut buf).unwrap();
assert!(memmem::find(&buf[..n], b"Password: ").is_some());
}
#[test]
fn test_raw_guard_1() {
let (_main, peer) = pty();
let before = lflag(&peer);
let fd = safe_open_abs(
peer.as_str(),
OFlag::O_RDWR | OFlag::O_NOCTTY,
ResolveFlag::empty(),
)
.unwrap();
let orig = tcgetattr2(fd.as_fd()).unwrap();
enable_raw_mode(fd.as_fd(), &orig).unwrap();
assert_eq!(lflag(&peer) & (ECHO | ICANON), 0);
drop(RawGuard::new(fd.as_fd(), orig));
assert_eq!(lflag(&peer), before);
}
#[test]
fn test_prompt_password_1() {
if safe_open_abs(
DEV_TTY,
OFlag::O_RDWR | OFlag::O_NONBLOCK | OFlag::O_NOCTTY,
ResolveFlag::empty(),
)
.is_err()
{
assert!(prompt_password("Password: ").is_err());
}
}
}