Skip to content

Commit 0e3d383

Browse files
authored
Merge commit from fork
Also disable canonical mode when pwfeedback is not enabled
2 parents e8043a7 + 1cf14c9 commit 0e3d383

File tree

2 files changed

+104
-94
lines changed

2 files changed

+104
-94
lines changed

src/pam/rpassword.rs

Lines changed: 103 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct HiddenInput {
3131
}
3232

3333
impl HiddenInput {
34-
fn new(feedback: bool) -> io::Result<Option<HiddenInput>> {
34+
fn new() -> io::Result<Option<HiddenInput>> {
3535
// control ourselves that we are really talking to a TTY
3636
// mitigates: https://marc.info/?l=oss-security&m=168164424404224
3737
let Ok(tty) = fs::File::open("/dev/tty") else {
@@ -51,10 +51,8 @@ impl HiddenInput {
5151
// But don't hide the NL character when the user hits ENTER.
5252
term.c_lflag |= ECHONL;
5353

54-
if feedback {
55-
// Disable canonical mode to read character by character when pwfeedback is enabled.
56-
term.c_lflag &= !ICANON;
57-
}
54+
// Disable canonical mode to read character by character when pwfeedback is enabled.
55+
term.c_lflag &= !ICANON;
5856

5957
// Save the settings for now.
6058
// SAFETY: we are passing tcsetattr a valid file descriptor and pointer-to-struct
@@ -82,30 +80,6 @@ fn safe_tcgetattr(tty: impl AsFd) -> io::Result<termios> {
8280
Ok(unsafe { term.assume_init() })
8381
}
8482

85-
/// Reads a password from the given file descriptor
86-
fn read_unbuffered(source: &mut dyn io::Read) -> io::Result<PamBuffer> {
87-
let mut password = PamBuffer::default();
88-
let mut pwd_iter = password.iter_mut();
89-
90-
const EOL: u8 = 0x0A;
91-
//TODO: we actually only want to allow clippy::unbuffered_bytes
92-
#[allow(clippy::perf)]
93-
let input = source.bytes().take_while(|x| x.as_ref().ok() != Some(&EOL));
94-
95-
for read_byte in input {
96-
if let Some(dest) = pwd_iter.next() {
97-
*dest = read_byte?
98-
} else {
99-
return Err(Error::new(
100-
ErrorKind::OutOfMemory,
101-
"incorrect password attempt",
102-
));
103-
}
104-
}
105-
106-
Ok(password)
107-
}
108-
10983
fn erase_feedback(sink: &mut dyn io::Write, i: usize) {
11084
const BACKSPACE: u8 = 0x08;
11185
for _ in 0..i {
@@ -115,14 +89,40 @@ fn erase_feedback(sink: &mut dyn io::Write, i: usize) {
11589
}
11690
}
11791

118-
/// Reads a password from the given file descriptor while showing feedback to the user.
119-
fn read_unbuffered_with_feedback(
92+
enum Hidden<'a> {
93+
No,
94+
Yes(&'a HiddenInput),
95+
WithFeedback(&'a HiddenInput),
96+
}
97+
98+
/// Reads a password from the given file descriptor while optionally showing feedback to the user.
99+
fn read_unbuffered(
120100
source: &mut dyn io::Read,
121101
sink: &mut dyn io::Write,
122-
hide_input: &HiddenInput,
102+
hide_input: Hidden<'_>,
123103
) -> io::Result<PamBuffer> {
104+
struct ExitGuard<'a> {
105+
pw_len: usize,
106+
feedback: bool,
107+
sink: &'a mut dyn io::Write,
108+
}
109+
110+
// Ensure we erase the password feedback no matter how we exit read_unbuffered
111+
impl Drop for ExitGuard<'_> {
112+
fn drop(&mut self) {
113+
if self.feedback {
114+
erase_feedback(self.sink, self.pw_len);
115+
}
116+
let _ = self.sink.write(b"\n");
117+
}
118+
}
119+
124120
let mut password = PamBuffer::default();
125-
let mut pw_len = 0;
121+
let mut state = ExitGuard {
122+
pw_len: 0,
123+
feedback: matches!(hide_input, Hidden::WithFeedback(_)),
124+
sink,
125+
};
126126

127127
// invariant: the amount of nonzero-bytes in the buffer correspond
128128
// with the amount of asterisks on the terminal (both tracked in `pw_len`)
@@ -132,41 +132,47 @@ fn read_unbuffered_with_feedback(
132132
let read_byte = read_byte?;
133133

134134
if read_byte == b'\n' || read_byte == b'\r' {
135-
erase_feedback(sink, pw_len);
136-
let _ = sink.write(b"\n");
137135
break;
138136
}
139137

140-
if read_byte == hide_input.term_orig.c_cc[VEOF] {
141-
erase_feedback(sink, pw_len);
142-
password.fill(0);
143-
break;
138+
if let Hidden::Yes(input) | Hidden::WithFeedback(input) = hide_input {
139+
if read_byte == input.term_orig.c_cc[VEOF] {
140+
password.fill(0);
141+
break;
142+
}
143+
144+
if read_byte == input.term_orig.c_cc[VERASE] {
145+
if state.pw_len > 0 {
146+
if let Hidden::WithFeedback(_) = hide_input {
147+
erase_feedback(state.sink, 1);
148+
}
149+
password[state.pw_len - 1] = 0;
150+
state.pw_len -= 1;
151+
}
152+
continue;
153+
}
154+
155+
if read_byte == input.term_orig.c_cc[VKILL] {
156+
if let Hidden::WithFeedback(_) = hide_input {
157+
erase_feedback(state.sink, state.pw_len);
158+
}
159+
password.fill(0);
160+
state.pw_len = 0;
161+
continue;
162+
}
144163
}
145164

146-
if read_byte == hide_input.term_orig.c_cc[VERASE] {
147-
if pw_len > 0 {
148-
erase_feedback(sink, 1);
149-
password[pw_len - 1] = 0;
150-
pw_len -= 1;
165+
if let Some(dest) = password.get_mut(state.pw_len) {
166+
*dest = read_byte;
167+
state.pw_len += 1;
168+
if let Hidden::WithFeedback(_) = hide_input {
169+
let _ = state.sink.write(b"*");
151170
}
152-
} else if read_byte == hide_input.term_orig.c_cc[VKILL] {
153-
erase_feedback(sink, pw_len);
154-
password.fill(0);
155-
pw_len = 0;
156171
} else {
157-
#[allow(clippy::collapsible_else_if)]
158-
if let Some(dest) = password.get_mut(pw_len) {
159-
*dest = read_byte;
160-
pw_len += 1;
161-
let _ = sink.write(b"*");
162-
} else {
163-
erase_feedback(sink, pw_len);
164-
165-
return Err(Error::new(
166-
ErrorKind::OutOfMemory,
167-
"incorrect password attempt",
168-
));
169-
}
172+
return Err(Error::new(
173+
ErrorKind::OutOfMemory,
174+
"incorrect password attempt",
175+
));
170176
}
171177
}
172178

@@ -269,34 +275,50 @@ impl Terminal<'_> {
269275

270276
/// Reads input with TTY echo disabled
271277
pub fn read_password(&mut self, timeout: Option<Duration>) -> io::Result<PamBuffer> {
272-
let mut input = self.source_timeout(timeout);
273-
let _hide_input = HiddenInput::new(false)?;
274-
read_unbuffered(&mut input)
278+
let hide_input = HiddenInput::new()?;
279+
self.read_inner(
280+
timeout,
281+
hide_input.as_ref().map(Hidden::Yes).unwrap_or(Hidden::No),
282+
)
275283
}
276284

277285
/// Reads input with TTY echo disabled, but do provide visual feedback while typing.
278286
pub fn read_password_with_feedback(
279287
&mut self,
280288
timeout: Option<Duration>,
281289
) -> io::Result<PamBuffer> {
282-
match (HiddenInput::new(true)?, self) {
283-
(Some(hide_input), Terminal::StdIE(stdin, stdout)) => {
290+
let hide_input = HiddenInput::new()?;
291+
self.read_inner(
292+
timeout,
293+
hide_input
294+
.as_ref()
295+
.map(Hidden::WithFeedback)
296+
.unwrap_or(Hidden::No),
297+
)
298+
}
299+
300+
/// Reads input with TTY echo enabled
301+
pub fn read_cleartext(&mut self) -> io::Result<PamBuffer> {
302+
self.read_inner(None, Hidden::No)
303+
}
304+
305+
fn read_inner(
306+
&mut self,
307+
timeout: Option<Duration>,
308+
hide_input: Hidden<'_>,
309+
) -> io::Result<PamBuffer> {
310+
match self {
311+
Terminal::StdIE(stdin, stdout) => {
284312
let mut reader = TimeoutRead::new(stdin.as_fd(), timeout);
285-
read_unbuffered_with_feedback(&mut reader, stdout, &hide_input)
313+
read_unbuffered(&mut reader, stdout, hide_input)
286314
}
287-
(Some(hide_input), Terminal::Tty(file)) => {
315+
Terminal::Tty(file) => {
288316
let mut reader = TimeoutRead::new(file.as_fd(), timeout);
289-
read_unbuffered_with_feedback(&mut reader, &mut &*file, &hide_input)
317+
read_unbuffered(&mut reader, &mut &*file, hide_input)
290318
}
291-
(None, term) => read_unbuffered(&mut term.source_timeout(timeout)),
292319
}
293320
}
294321

295-
/// Reads input with TTY echo enabled
296-
pub fn read_cleartext(&mut self) -> io::Result<PamBuffer> {
297-
read_unbuffered(self.source())
298-
}
299-
300322
/// Display information
301323
pub fn prompt(&mut self, text: &str) -> io::Result<()> {
302324
write_unbuffered(self.sink(), text.as_bytes())
@@ -309,20 +331,6 @@ impl Terminal<'_> {
309331
}
310332

311333
// boilerplate reduction functions
312-
fn source(&mut self) -> &mut dyn io::Read {
313-
match self {
314-
Terminal::StdIE(x, _) => x,
315-
Terminal::Tty(x) => x,
316-
}
317-
}
318-
319-
fn source_timeout(&self, timeout: Option<Duration>) -> TimeoutRead<'_> {
320-
match self {
321-
Terminal::StdIE(stdin, _) => TimeoutRead::new(stdin.as_fd(), timeout),
322-
Terminal::Tty(file) => TimeoutRead::new(file.as_fd(), timeout),
323-
}
324-
}
325-
326334
fn sink(&mut self) -> &mut dyn io::Write {
327335
match self {
328336
Terminal::StdIE(_, x) => x,
@@ -333,12 +341,13 @@ impl Terminal<'_> {
333341

334342
#[cfg(test)]
335343
mod test {
336-
use super::{read_unbuffered, write_unbuffered};
344+
use super::*;
337345

338346
#[test]
339347
fn miri_test_read() {
340348
let mut data = "password123\nhello world".as_bytes();
341-
let buf = read_unbuffered(&mut data).unwrap();
349+
let mut stdout = Vec::new();
350+
let buf = read_unbuffered(&mut data, &mut stdout, Hidden::No).unwrap();
342351
// check that the \n is not part of input
343352
assert_eq!(
344353
buf.iter()
@@ -353,8 +362,9 @@ mod test {
353362

354363
#[test]
355364
fn miri_test_longpwd() {
356-
assert!(read_unbuffered(&mut "a".repeat(511).as_bytes()).is_ok());
357-
assert!(read_unbuffered(&mut "a".repeat(512).as_bytes()).is_err());
365+
let mut stdout = Vec::new();
366+
assert!(read_unbuffered(&mut "a".repeat(511).as_bytes(), &mut stdout, Hidden::No).is_ok());
367+
assert!(read_unbuffered(&mut "a".repeat(512).as_bytes(), &mut stdout, Hidden::No).is_err());
358368
}
359369

360370
#[test]

test-framework/sudo-compliance-tests/src/sudo/nopasswd.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ fn v_flag_without_pwd_fails_if_nopasswd_is_not_set_for_all_users_entries() {
143143
} else {
144144
assert_contains!(
145145
stderr,
146-
"[sudo: authenticate] Password: sudo: Authentication failed, try again.\n[sudo: authenticate] Password: sudo: Authentication failed, try again.\n[sudo: authenticate] Password: sudo-rs: Maximum 3 incorrect authentication attempts"
146+
"[sudo: authenticate] Password: \nsudo: Authentication failed, try again.\n[sudo: authenticate] Password: \nsudo: Authentication failed, try again.\n[sudo: authenticate] Password: \nsudo-rs: Maximum 3 incorrect authentication attempts"
147147
);
148148
}
149149
}

0 commit comments

Comments
 (0)