@@ -31,7 +31,7 @@ struct HiddenInput {
3131}
3232
3333impl 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-
10983fn 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) ]
335343mod test {
336- use super :: { read_unbuffered , write_unbuffered } ;
344+ use super :: * ;
337345
338346 #[ test]
339347 fn miri_test_read ( ) {
340348 let mut data = "password123\n hello 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]
0 commit comments