Skip to main content

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//! For testing or custom use-cases, you can use `read_password_with_config` and `prompt_password_with_config`:
17//! ```
18//! use std::io::{Cursor, Write};
19//!
20//! let config = rpassword::ConfigBuilder::new()
21//!     // Default input is the console, but we can pass any file path or raw data
22//!     .input_data("my-password\n")
23//!     // Default output is the console, but we can also discard it
24//!     .output_discard()
25//!     // Default behavior is to hide the password as it's being typed, but we can change that
26//!     .password_feedback_mask('*')
27//!     .build();
28//!
29//! let password = rpassword::read_password_with_config(config).unwrap();
30//! println!("Your password is {}", password);
31//! ```
32
33use rtoolbox::fix_line_issues::fix_line_issues;
34use rtoolbox::print_tty::print_writer;
35use rtoolbox::safe_string::SafeString;
36use std::fs::OpenOptions;
37use std::io;
38use std::io::{BufRead, Cursor, Write};
39
40mod config;
41mod feedback;
42
43#[cfg(all(target_family = "unix", not(target_family = "wasm")))]
44mod unix;
45#[cfg(all(target_family = "unix", not(target_family = "wasm")))]
46use unix::*;
47
48#[cfg(target_family = "windows")]
49mod windows;
50#[cfg(target_family = "windows")]
51use windows::*;
52
53mod utf8;
54#[cfg(target_family = "wasm")]
55mod wasm;
56
57#[cfg(target_family = "wasm")]
58use wasm::*;
59
60use crate::config::{OutputTarget, PasswordFeedback};
61use crate::feedback::FeedbackState;
62pub use config::{Config, ConfigBuilder};
63
64const BACKSPACE: char = '\x08';
65const DEL: char = '\x7F';
66const CTRL_C: char = '\x03';
67const CTRL_D: char = '\x04';
68const CTRL_U: char = '\x15';
69const CTRL_W: char = '\x17';
70const ESC: char = '\x1B';
71
72trait RawPasswordInput {
73    fn new(config: Config) -> io::Result<impl RawPasswordInput>;
74    fn needs_terminal_configuration(&self) -> bool;
75    fn apply_terminal_configuration(&mut self) -> io::Result<()>;
76    fn read_char(&mut self) -> std::io::Result<char>;
77    fn write_output(&mut self, output: &str) -> std::io::Result<()>;
78    fn send_signal_sigint(&mut self) -> std::io::Result<()>;
79
80    /// Reads a password from the console using the given config
81    fn read_password(&mut self, password_feedback: PasswordFeedback) -> std::io::Result<String> {
82        if self.needs_terminal_configuration() {
83            self.apply_terminal_configuration()?;
84        }
85
86        let mut state = FeedbackState::new(password_feedback, self.needs_terminal_configuration());
87
88        loop {
89            let c = match self.read_char() {
90                Ok(c) => c,
91                Err(e) => {
92                    if e.kind() == std::io::ErrorKind::UnexpectedEof {
93                        break;
94                    }
95                    return Err(e);
96                }
97            };
98            match c {
99                // LF / CR (Enter)
100                '\n' | '\r' => {
101                    let output = state.finish();
102                    if !output.is_empty() {
103                        self.write_output(output.as_str())?;
104                    }
105                    break;
106                }
107                // Backspace / DEL
108                DEL | BACKSPACE => {
109                    let output = state.pop_char();
110                    if !output.is_empty() {
111                        self.write_output(output.as_str())?;
112                    }
113                }
114                // Ctrl-U: clear line
115                CTRL_U => {
116                    let output = state.clear();
117                    if !output.is_empty() {
118                        self.write_output(output.as_str())?;
119                    }
120                }
121                // Ctrl-W: clear to last space
122                CTRL_W => {
123                    let output = state.clear_til_last_space();
124                    if !output.is_empty() {
125                        self.write_output(output.as_str())?;
126                    }
127                }
128                // Ctrl-C: interrupt
129                CTRL_C => {
130                    let output = state.abort();
131                    if !output.is_empty() {
132                        self.write_output(output.as_str())?;
133                    }
134                    self.send_signal_sigint()?;
135                    return Err(std::io::Error::new(
136                        std::io::ErrorKind::Interrupted,
137                        "interrupted",
138                    ));
139                }
140                // Ctrl-D: EOF when empty
141                CTRL_D => {
142                    if state.is_empty() {
143                        return Err(std::io::Error::new(
144                            std::io::ErrorKind::UnexpectedEof,
145                            "unexpected end of file",
146                        ));
147                    }
148                }
149                // ESC: consume and discard escape sequence like arrow keys
150                ESC => {
151                    let c = match self.read_char() {
152                        Ok(c) => c,
153                        Err(e) => {
154                            if e.kind() == std::io::ErrorKind::UnexpectedEof {
155                                break;
156                            }
157                            return Err(e);
158                        }
159                    };
160
161                    if c == '[' || c == 'O' {
162                        // CSI (ESC [) or SS3 (ESC O): read until final byte (0x40-0x7E)
163                        loop {
164                            let c = match self.read_char() {
165                                Ok(c) => c,
166                                Err(e) => {
167                                    if e.kind() == std::io::ErrorKind::UnexpectedEof {
168                                        break;
169                                    }
170                                    return Err(e);
171                                }
172                            };
173                            if ('\x40'..='\x7E').contains(&c) {
174                                break;
175                            }
176                        }
177                    }
178                }
179                c if !c.is_control() => {
180                    let output = state.push_char(c);
181                    if !output.is_empty() {
182                        self.write_output(output.as_str())?;
183                    }
184                }
185                // Discard unrecognized control characters and invalid input
186                _ => {}
187            }
188        }
189
190        Ok(state.into_password())
191    }
192}
193
194/// Reads a password from `impl BufRead`.
195///
196/// **Deprecated**: This method is deprecated. Use `read_password_with_config` with a temporary file instead.
197/// See the example below for updated usage.
198///
199/// # Example of Updated Usage
200/// ```
201/// use std::io::{Cursor, Write};
202/// use rpassword::{read_password_with_config, ConfigBuilder};
203///
204/// let config = ConfigBuilder::new()
205///     .input_reader(Cursor::new("my-password\n")) // anything that implements Read is OK here
206///     .output_discard()
207///     .build();
208///
209/// let password = read_password_with_config(config).unwrap();
210/// println!("The typed password is: {}", password);
211/// ```
212#[deprecated(
213    since = "7.5.0",
214    note = "Use `read_password_with_config` with `ConfigBuilder::input_reader` instead."
215)]
216pub fn read_password_from_bufread(reader: &mut impl BufRead) -> std::io::Result<String> {
217    let mut password = SafeString::new();
218    reader.read_line(&mut password)?;
219
220    fix_line_issues(password.into_inner())
221}
222
223/// Prompts on `impl Write` and then reads a password from `impl BufRead`.
224///
225/// **Deprecated**: This method is deprecated. Use `prompt_password_with_config` with a temporary file instead.
226/// See the example below for updated usage.
227///
228/// # Example of Updated Usage
229/// ```
230/// use std::io::{Cursor, Write};
231/// use rpassword::{prompt_password_with_config, ConfigBuilder};
232///
233/// let mut input = Cursor::new(b"my-password\n".to_vec());
234///
235/// let config = ConfigBuilder::new()
236///     .input_reader(Cursor::new("my-password\n")) // anything that implements Read is OK here
237///     .output_writer(Cursor::new(Vec::<u8>::new())) // anything that implements Write is OK here
238///     .build();
239///
240/// let password = prompt_password_with_config("Your password: ", config).unwrap();
241/// println!("The typed password is: {}", password);
242/// ```
243#[deprecated(
244    since = "7.5.0",
245    note = "Use `prompt_password_with_config` with `ConfigBuilder::input_reader` and `ConfigBuilder::output_writer()` instead."
246)]
247#[allow(deprecated)]
248pub fn prompt_password_from_bufread(
249    reader: &mut impl BufRead,
250    writer: &mut impl Write,
251    prompt: impl ToString,
252) -> std::io::Result<String> {
253    print_writer(writer, prompt.to_string().as_str())
254        .and_then(|_| read_password_from_bufread(reader))
255}
256
257/// Reads a password from TTY using the given config
258pub fn read_password_with_config(config: Config) -> std::io::Result<String> {
259    let password_feedback = config.password_feedback;
260    let mut raw_mode_input = RawModeInput::new(config)?;
261    raw_mode_input.read_password(password_feedback)
262}
263
264/// Reads a password from the TTY
265pub fn read_password() -> std::io::Result<String> {
266    read_password_with_config(ConfigBuilder::default().build())
267}
268
269/// Prompts on the TTY and then reads a password from TTY
270pub fn prompt_password(prompt: impl ToString) -> std::io::Result<String> {
271    prompt_password_with_config(prompt, ConfigBuilder::new().build())
272}
273
274/// Prompts and then reads a password using the given config
275pub fn prompt_password_with_config(
276    prompt: impl ToString,
277    mut config: Config,
278) -> std::io::Result<String> {
279    {
280        // Create an inner scope to allow using the config without moving it
281        // The mut ref to the config is dropped at the end of the scope
282        let mut output: Box<dyn Write> = match &mut config.output {
283            OutputTarget::FilePath(path) => Box::new(OpenOptions::new().write(true).open(path)?),
284            OutputTarget::Writer(writer) => Box::new(writer),
285            OutputTarget::Void => Box::new(Cursor::new(Vec::<u8>::new())), // TODO: Should use a SafeVec instead
286        };
287        output.write_all(prompt.to_string().as_bytes())?;
288        output.flush()?;
289    }
290
291    read_password_with_config(config)
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use std::io::Cursor;
298
299    fn mock_input_crlf() -> Cursor<&'static [u8]> {
300        Cursor::new(&b"A mocked response.\r\nAnother mocked response.\r\n"[..])
301    }
302
303    fn mock_input_lf() -> Cursor<&'static [u8]> {
304        Cursor::new(&b"A mocked response.\nAnother mocked response.\n"[..])
305    }
306
307    #[test]
308    #[allow(deprecated)]
309    fn can_read_from_redirected_input_many_times() {
310        let mut reader_crlf = mock_input_crlf();
311
312        let response = read_password_from_bufread(&mut reader_crlf).unwrap();
313        assert_eq!(response, "A mocked response.");
314        let response = read_password_from_bufread(&mut reader_crlf).unwrap();
315        assert_eq!(response, "Another mocked response.");
316
317        let mut reader_lf = mock_input_lf();
318        let response = read_password_from_bufread(&mut reader_lf).unwrap();
319        assert_eq!(response, "A mocked response.");
320        let response = read_password_from_bufread(&mut reader_lf).unwrap();
321        assert_eq!(response, "Another mocked response.");
322    }
323
324    #[test]
325    fn test_read_password_with_config_with_input_file() {
326        let mut temp_file = tempfile::NamedTempFile::new().unwrap();
327        temp_file.write_all(b"password\n").unwrap();
328        let path = temp_file.path().to_str().unwrap().to_string();
329
330        let config = ConfigBuilder::new()
331            .input_file_path(path.as_str())
332            .output_discard()
333            .build();
334
335        let result = read_password_with_config(config);
336        assert_eq!("password", result.unwrap());
337    }
338
339    #[test]
340    fn test_read_password_with_config_with_input_cursor() {
341        let config = ConfigBuilder::new()
342            .input_data("hello world\x7F\x7F\x7F\n")
343            .output_discard()
344            .build();
345
346        let result = read_password_with_config(config);
347        assert_eq!("hello wo", result.unwrap());
348    }
349}