rpassword/
lib.rs

1//! This library makes it easy to read passwords in a console application on all platforms, Unix,
2//! Windows, WASM, etc.
3//!
4//! Here's how you can read a password:
5//! ```no_run
6//! let password = rpassword::read_password().unwrap();
7//! println!("Your password is {}", password);
8//! ```
9//!
10//! You can also prompt for a password:
11//! ```no_run
12//! let password = rpassword::prompt_password("Your password: ").unwrap();
13//! println!("Your password is {}", password);
14//! ```
15//!
16//! Finally, in unit tests, you might want to pass a `Cursor`, which implements `BufRead`. In that
17//! case, you can use `read_password_from_bufread` and `prompt_password_from_bufread`:
18//! ```
19//! use std::io::Cursor;
20//!
21//! let mut mock_input = Cursor::new("my-password\n".as_bytes().to_owned());
22//! let password = rpassword::read_password_from_bufread(&mut mock_input).unwrap();
23//! println!("Your password is {}", password);
24//!
25//! let mut mock_input = Cursor::new("my-password\n".as_bytes().to_owned());
26//! let mut mock_output = Cursor::new(Vec::new());
27//! let password = rpassword::prompt_password_from_bufread(&mut mock_input, &mut mock_output, "Your password: ").unwrap();
28//! println!("Your password is {}", password);
29//! ```
30
31use rtoolbox::fix_line_issues::fix_line_issues;
32use rtoolbox::print_tty::{print_tty, print_writer};
33use rtoolbox::safe_string::SafeString;
34use std::io::{BufRead, Write};
35
36#[cfg(target_family = "wasm")]
37mod wasm {
38    use std::io::{self, BufRead};
39
40    /// Reads a password from the TTY
41    pub fn read_password() -> std::io::Result<String> {
42        let tty = std::fs::File::open("/dev/tty")?;
43        let mut reader = io::BufReader::new(tty);
44
45        read_password_from_fd_with_hidden_input(&mut reader)
46    }
47
48    /// Reads a password from a given file descriptor
49    fn read_password_from_fd_with_hidden_input(
50        reader: &mut impl BufRead,
51    ) -> std::io::Result<String> {
52        let mut password = super::SafeString::new();
53
54        reader.read_line(&mut password)?;
55        super::fix_line_issues(password.into_inner())
56    }
57}
58
59#[cfg(target_family = "unix")]
60mod unix {
61    use libc::{c_int, tcsetattr, termios, ECHO, ECHONL, TCSANOW};
62    use std::io::{self, BufRead};
63    use std::mem;
64    use std::os::unix::io::AsRawFd;
65
66    struct HiddenInput {
67        fd: i32,
68        term_orig: termios,
69    }
70
71    impl HiddenInput {
72        fn new(fd: i32) -> io::Result<HiddenInput> {
73            // Make two copies of the terminal settings. The first one will be modified
74            // and the second one will act as a backup for when we want to set the
75            // terminal back to its original state.
76            let mut term = safe_tcgetattr(fd)?;
77            let term_orig = safe_tcgetattr(fd)?;
78
79            // Hide the password. This is what makes this function useful.
80            term.c_lflag &= !ECHO;
81
82            // But don't hide the NL character when the user hits ENTER.
83            term.c_lflag |= ECHONL;
84
85            // Save the settings for now.
86            io_result(unsafe { tcsetattr(fd, TCSANOW, &term) })?;
87
88            Ok(HiddenInput { fd, term_orig })
89        }
90    }
91
92    impl Drop for HiddenInput {
93        fn drop(&mut self) {
94            // Set the the mode back to normal
95            unsafe {
96                tcsetattr(self.fd, TCSANOW, &self.term_orig);
97            }
98        }
99    }
100
101    /// Turns a C function return into an IO Result
102    fn io_result(ret: c_int) -> std::io::Result<()> {
103        match ret {
104            0 => Ok(()),
105            _ => Err(std::io::Error::last_os_error()),
106        }
107    }
108
109    fn safe_tcgetattr(fd: c_int) -> std::io::Result<termios> {
110        let mut term = mem::MaybeUninit::<termios>::uninit();
111        io_result(unsafe { ::libc::tcgetattr(fd, term.as_mut_ptr()) })?;
112        Ok(unsafe { term.assume_init() })
113    }
114
115    /// Reads a password from the TTY
116    pub fn read_password() -> std::io::Result<String> {
117        let tty = std::fs::File::open("/dev/tty")?;
118        let fd = tty.as_raw_fd();
119        let mut reader = io::BufReader::new(tty);
120
121        read_password_from_fd_with_hidden_input(&mut reader, fd)
122    }
123
124    /// Reads a password from a given file descriptor
125    fn read_password_from_fd_with_hidden_input(
126        reader: &mut impl BufRead,
127        fd: i32,
128    ) -> std::io::Result<String> {
129        let mut password = super::SafeString::new();
130
131        let hidden_input = HiddenInput::new(fd)?;
132
133        reader.read_line(&mut password)?;
134
135        std::mem::drop(hidden_input);
136
137        super::fix_line_issues(password.into_inner())
138    }
139}
140
141#[cfg(target_family = "windows")]
142mod windows {
143    use std::io::BufRead;
144    use std::io::{self, BufReader};
145    use std::os::windows::io::FromRawHandle;
146    use windows_sys::core::PCSTR;
147    use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE};
148    use windows_sys::Win32::Storage::FileSystem::{
149        CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
150    };
151    use windows_sys::Win32::System::Console::{
152        GetConsoleMode, SetConsoleMode, CONSOLE_MODE, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
153    };
154
155    struct HiddenInput {
156        mode: u32,
157        handle: HANDLE,
158    }
159
160    impl HiddenInput {
161        fn new(handle: HANDLE) -> io::Result<HiddenInput> {
162            let mut mode = 0;
163
164            // Get the old mode so we can reset back to it when we are done
165            if unsafe { GetConsoleMode(handle, &mut mode as *mut CONSOLE_MODE) } == 0 {
166                return Err(std::io::Error::last_os_error());
167            }
168
169            // We want to be able to read line by line, and we still want backspace to work
170            let new_mode_flags = ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT;
171            if unsafe { SetConsoleMode(handle, new_mode_flags) } == 0 {
172                return Err(std::io::Error::last_os_error());
173            }
174
175            Ok(HiddenInput { mode, handle })
176        }
177    }
178
179    impl Drop for HiddenInput {
180        fn drop(&mut self) {
181            // Set the the mode back to normal
182            unsafe {
183                SetConsoleMode(self.handle, self.mode);
184            }
185        }
186    }
187
188    /// Reads a password from the TTY
189    pub fn read_password() -> std::io::Result<String> {
190        let handle = unsafe {
191            CreateFileA(
192                b"CONIN$\x00".as_ptr() as PCSTR,
193                GENERIC_READ | GENERIC_WRITE,
194                FILE_SHARE_READ | FILE_SHARE_WRITE,
195                std::ptr::null(),
196                OPEN_EXISTING,
197                0,
198                INVALID_HANDLE_VALUE,
199            )
200        };
201
202        if handle == INVALID_HANDLE_VALUE {
203            return Err(std::io::Error::last_os_error());
204        }
205
206        let mut stream = BufReader::new(unsafe { std::fs::File::from_raw_handle(handle as _) });
207        read_password_from_handle_with_hidden_input(&mut stream, handle)
208    }
209
210    /// Reads a password from a given file handle
211    fn read_password_from_handle_with_hidden_input(
212        reader: &mut impl BufRead,
213        handle: HANDLE,
214    ) -> io::Result<String> {
215        let mut password = super::SafeString::new();
216
217        let hidden_input = HiddenInput::new(handle)?;
218
219        let reader_return = reader.read_line(&mut password);
220
221        // Newline for windows which otherwise prints on the same line.
222        println!();
223
224        if reader_return.is_err() {
225            return Err(reader_return.unwrap_err());
226        }
227
228        std::mem::drop(hidden_input);
229
230        super::fix_line_issues(password.into_inner())
231    }
232}
233
234#[cfg(target_family = "unix")]
235pub use unix::read_password;
236#[cfg(target_family = "wasm")]
237pub use wasm::read_password;
238#[cfg(target_family = "windows")]
239pub use windows::read_password;
240
241/// Reads a password from anything that implements BufRead
242pub fn read_password_from_bufread(reader: &mut impl BufRead) -> std::io::Result<String> {
243    let mut password = SafeString::new();
244    reader.read_line(&mut password)?;
245
246    fix_line_issues(password.into_inner())
247}
248
249/// Prompts on the TTY and then reads a password from anything that implements BufRead
250pub fn prompt_password_from_bufread(
251    reader: &mut impl BufRead,
252    writer: &mut impl Write,
253    prompt: impl ToString,
254) -> std::io::Result<String> {
255    print_writer(writer, prompt.to_string().as_str())
256        .and_then(|_| read_password_from_bufread(reader))
257}
258
259/// Prompts on the TTY and then reads a password from TTY
260pub fn prompt_password(prompt: impl ToString) -> std::io::Result<String> {
261    print_tty(prompt.to_string().as_str()).and_then(|_| read_password())
262}
263
264#[cfg(test)]
265mod tests {
266    use std::io::Cursor;
267
268    fn mock_input_crlf() -> Cursor<&'static [u8]> {
269        Cursor::new(&b"A mocked response.\r\nAnother mocked response.\r\n"[..])
270    }
271
272    fn mock_input_lf() -> Cursor<&'static [u8]> {
273        Cursor::new(&b"A mocked response.\nAnother mocked response.\n"[..])
274    }
275
276    #[test]
277    fn can_read_from_redirected_input_many_times() {
278        let mut reader_crlf = mock_input_crlf();
279
280        let response = super::read_password_from_bufread(&mut reader_crlf).unwrap();
281        assert_eq!(response, "A mocked response.");
282        let response = super::read_password_from_bufread(&mut reader_crlf).unwrap();
283        assert_eq!(response, "Another mocked response.");
284
285        let mut reader_lf = mock_input_lf();
286        let response = super::read_password_from_bufread(&mut reader_lf).unwrap();
287        assert_eq!(response, "A mocked response.");
288        let response = super::read_password_from_bufread(&mut reader_lf).unwrap();
289        assert_eq!(response, "Another mocked response.");
290    }
291}