readpassphrase_3/
lib.rs

1//! Lightweight, easy-to-use wrapper around the C [`readpassphrase(3)`][0] function.
2//!
3//! From the man page:
4//! > The `readpassphrase()` function displays a prompt to, and reads in a passphrase from,
5//! > `/dev/tty`. If this file is inaccessible and the [`RPP_REQUIRE_TTY`](Flags::REQUIRE_TTY) flag
6//! > is not set, `readpassphrase()` displays the prompt on the standard error output and reads
7//! > from the standard input.
8//!
9//! # Usage
10//! For the simplest of cases, where you would just like to read a password from the console into a
11//! [`String`] to use elsewhere, you can use [`getpass`]:
12//! ```no_run
13//! use readpassphrase_3::getpass;
14//! let _ = getpass(c"Enter your password: ").expect("failed reading password");
15//! ```
16//!
17//! If you need to pass [`Flags`] or to control the buffer size, then you can use
18//! [`readpassphrase`] or [`readpassphrase_into`] depending on your ownership requirements:
19//! ```no_run
20//! let mut buf = vec![0u8; 256];
21//! use readpassphrase_3::{Flags, readpassphrase};
22//! let pass: &str = readpassphrase(c"Password: ", &mut buf, Flags::default()).unwrap();
23//!
24//! use readpassphrase_3::readpassphrase_into;
25//! let pass: String = readpassphrase_into(c"Pass: ", buf, Flags::FORCELOWER).unwrap();
26//! # _ = pass;
27//! ```
28//!
29//! # Security
30//! The [`readpassphrase(3)` man page][0] says:
31//! > The calling process should zero the passphrase as soon as possible to avoid leaving the
32//! > cleartext passphrase visible in the process's address space.
33//!
34//! It is your job to ensure that this is done with the data you own, i.e.
35//! any [`Vec`] passed to [`readpassphrase`] or any [`String`] received from [`getpass`] or
36//! [`readpassphrase_into`].
37//!
38//! This crate ships with a minimal [`Zeroize`] trait that may be used for this purpose:
39//! ```no_run
40//! # use readpassphrase_3::{Flags, getpass, readpassphrase, readpassphrase_into};
41//! use readpassphrase_3::Zeroize;
42//! let mut pass = getpass(c"password: ").unwrap();
43//! // do_something_with(&pass);
44//! pass.zeroize();
45//!
46//! let mut buf = vec![0u8; 256];
47//! let res = readpassphrase(c"password: ", &mut buf, Flags::empty());
48//! // match_something_on(res);
49//! buf.zeroize();
50//!
51//! let mut pass = readpassphrase_into(c"password: ", buf, Flags::empty()).unwrap();
52//! // do_something_with(&pass);
53//! pass.zeroize();
54//! ```
55//!
56//! ## Zeroizing memory
57//! This crate works well with the [`zeroize`] crate. For example, [`zeroize::Zeroizing`] may be
58//! used to zero buffer contents regardless of a function’s control flow:
59//! ```no_run
60//! # use readpassphrase_3::{Error, Flags, PASSWORD_LEN, getpass, readpassphrase};
61//! use zeroize::Zeroizing;
62//! # fn main() -> Result<(), Error> {
63//! let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
64//! let pass = readpassphrase(c"pass: ", &mut buf, Flags::REQUIRE_TTY)?;
65//! // do_something_that_can_fail_with(pass)?;
66//!
67//! // Or alternatively:
68//! let pass = Zeroizing::new(getpass(c"pass: ")?);
69//! // do_something_that_can_fail_with(&pass)?;
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! If this crate’s `zeroize` feature is enabled, then its [`Zeroize`] will be replaced by a
75//! re-export of the upstream [`zeroize::Zeroize`].
76//!
77//! # “Mismatched types” errors
78//! The prompt strings in this API are <code>&[CStr]</code>, not <code>&[str]</code>.
79//! This is because the underlying C function assumes that the prompt is a null-terminated string;
80//! were we to take `&str` instead of `&CStr`, we would need to make a copy of the prompt on every
81//! call.
82//!
83//! Most of the time, your prompts will be string literals; you can ask Rust to give you a `&CStr`
84//! literal by simply prepending `c` to the string:
85//! ```no_run
86//! # use readpassphrase_3::{Error, getpass};
87//! # fn main() -> Result<(), Error> {
88//! let _ = getpass(c"pass: ")?;
89//! //              ^
90//! //              |
91//! //              like this
92//! # Ok(())
93//! # }
94//! ```
95//!
96//! If you need a dynamic prompt, look at [`CString`](std::ffi::CString).
97//!
98//! # Windows Limitations
99//! The Windows implementation of `readpassphrase(3)` that we are using does not yet support UTF-8
100//! in prompts; they must be ASCII. It also does not yet support flags, and always behaves as
101//! though called with [`Flags::empty()`].
102//!
103//! [0]: https://man.openbsd.org/readpassphrase
104//! [str]: prim@str "str"
105
106use std::{cmp, error, ffi::CStr, fmt, io, mem, str};
107
108use bitflags::bitflags;
109#[cfg(any(docsrs, not(feature = "zeroize")))]
110pub use our_zeroize::Zeroize;
111#[cfg(all(not(docsrs), feature = "zeroize"))]
112pub use zeroize::Zeroize;
113
114/// Size of buffer used in [`getpass`].
115///
116/// Because `readpassphrase(3)` null-terminates its string, the actual maximum password length for
117/// [`getpass`] is 255.
118pub const PASSWORD_LEN: usize = 256;
119
120/// Maximum capacity to use with `readpassphrase_into`.
121///
122/// Vectors with allocations larger than this will only have their capacity used up to this limit.
123/// (The initialized portion of the input vector is always used regardless of size.)
124pub const MAX_CAPACITY: usize = 1 << 12;
125
126bitflags! {
127    /// Flags for controlling readpassphrase.
128    ///
129    /// The default flag `ECHO_OFF` is not represented here because `bitflags` [recommends against
130    /// zero-bit flags][0]; it may be specified as either [`Flags::empty()`] or
131    /// [`Flags::default()`].
132    ///
133    /// Note that the Windows `readpassphrase(3)` implementation always acts like it has been
134    /// passed `ECHO_OFF`, i.e., the flags are ignored.
135    ///
136    /// [0]: https://docs.rs/bitflags/latest/bitflags/#zero-bit-flags
137    #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
138    pub struct Flags: i32 {
139        /// Leave echo on.
140        const ECHO_ON     = 0x01;
141        /// Fail if there is no tty.
142        const REQUIRE_TTY = 0x02;
143        /// Force input to lower case.
144        const FORCELOWER  = 0x04;
145        /// Force input to upper case.
146        const FORCEUPPER  = 0x08;
147        /// Strip the high bit from input.
148        const SEVENBIT    = 0x10;
149        /// Read from stdin, not `/dev/tty`.
150        const STDIN       = 0x20;
151    }
152}
153
154/// Errors that can occur in readpassphrase.
155#[derive(Debug)]
156pub enum Error {
157    /// `readpassphrase(3)` itself encountered an error.
158    Io(io::Error),
159    /// The entered password was not UTF-8.
160    Utf8(str::Utf8Error),
161}
162
163/// Reads a passphrase using `readpassphrase(3)`.
164///
165/// This function returns a <code>&[str]</code> backed by `buf`, representing a password of up to
166/// `buf.len() - 1` bytes. Any additional characters and the terminating newline are discarded.
167///
168/// # Errors
169/// Returns [`Err`] if `readpassphrase(3)` itself failed or if the entered password is not UTF-8.
170/// The former will be represented by [`Error::Io`] and the latter by [`Error::Utf8`].
171///
172/// # Security
173/// The passed buffer might contain sensitive data, even if this function returns an error.
174/// Therefore it should be zeroed as soon as possible. This can be achieved, for example, with
175/// [`zeroize::Zeroizing`]:
176/// ```no_run
177/// # use readpassphrase_3::{PASSWORD_LEN, Error, Flags, readpassphrase};
178/// use zeroize::Zeroizing;
179/// # fn main() -> Result<(), Error> {
180/// let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
181/// let pass = readpassphrase(c"Pass: ", &mut buf, Flags::default())?;
182/// # Ok(())
183/// # }
184/// ```
185/// [str]: prim@str "str"
186pub fn readpassphrase<'a>(
187    prompt: &CStr,
188    buf: &'a mut [u8],
189    flags: Flags,
190) -> Result<&'a str, Error> {
191    #[cfg(debug_assertions)]
192    {
193        // Fill `buf` with nonzero bytes to check that `ffi::readpassphrase` wrote a nul.
194        buf.fill(1);
195    }
196    let prompt = prompt.as_ptr();
197    let buf_ptr = buf.as_mut_ptr().cast();
198    let bufsiz = buf.len();
199    let flags = flags.bits();
200    // SAFETY: `prompt` is a nul-terminated byte sequence, and `buf_ptr` is an allocation of at
201    // least `bufsiz` bytes, by construction from `&CStr` and `&mut [u8]` respectively.
202    let res = unsafe { ffi::readpassphrase(prompt, buf_ptr, bufsiz, flags) };
203    if res.is_null() {
204        return Err(io::Error::last_os_error().into());
205    }
206    Ok(CStr::from_bytes_until_nul(buf).unwrap().to_str()?)
207}
208
209/// Reads a passphrase using `readpassphrase(3)`, returning a [`String`].
210///
211/// Internally, this function uses a buffer of [`PASSWORD_LEN`] bytes, allowing for passwords up to
212/// `PASSWORD_LEN - 1` characters (accounting for the C null terminator.) If the entered passphrase
213/// is longer, it will be truncated to the maximum length.
214///
215/// # Errors
216/// Returns [`Err`] if `readpassphrase(3)` itself failed or if the entered password is not UTF-8.
217/// The former will be represented by [`Error::Io`] and the latter by [`Error::Utf8`].
218///
219/// # Security
220/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
221/// to clear it when you are done with it:
222/// ```no_run
223/// # use readpassphrase_3::{Error, Zeroize, getpass};
224/// # fn main() -> Result<(), Error> {
225/// let mut pass = getpass(c"Pass: ")?;
226/// _ = pass;
227/// pass.zeroize();
228/// # Ok(())
229/// # }
230/// ```
231pub fn getpass(prompt: &CStr) -> Result<String, Error> {
232    let buf = Vec::with_capacity(PASSWORD_LEN);
233    Ok(readpassphrase_into(prompt, buf, Flags::empty())?)
234}
235
236/// An [`Error`] from [`readpassphrase_into`] containing the passed buffer.
237///
238/// The buffer is accessible via [`IntoError::into_bytes`][0], and the `Error` via
239/// [`IntoError::error`].
240///
241/// If [`into_bytes`][0] is not called, the buffer is automatically zeroed on drop.
242///
243/// [0]: IntoError::into_bytes
244#[derive(Debug)]
245pub struct IntoError(Error, Option<Vec<u8>>);
246
247/// Reads a passphrase using `readpassphrase(3)`, returning `buf` as a [`String`].
248///
249/// The returned [`String`] reuses `buf`’s memory; no copies are made, and `buf` is never
250/// reallocated.
251///
252/// `buf`’s full allocation will be  used, whether initialized or not, up to [4KiB][MAX_CAPACITY];
253/// i.e., the following two statements are equivalent:
254/// ```no_run
255/// # use readpassphrase_3::{Flags, readpassphrase_into};
256/// # let flags = Flags::empty();
257/// let _ = readpassphrase_into(c"> ", vec![0u8; 128], flags);
258/// let _ = readpassphrase_into(c"> ", Vec::with_capacity(128), flags);
259/// ```
260///
261/// If for some reason you must use this function to read more than 4KiB of text, then you should
262/// initialize the buffer to the length you will need.
263///
264/// # Errors
265/// Returns [`Err`] if `readpassphrase(3)` itself failed or if the entered password is not UTF-8.
266/// The former will be represented by [`Error::Io`] and the latter by [`Error::Utf8`]. The vector
267/// you moved in is also included, and in the case of [`Error::Utf8`], contains the non-UTF8 byte
268/// sequence produced by `readpassphrase(3)`.
269///
270/// See the docs for [`IntoError`] for more details on what you can do with this error.
271///
272/// # Security
273/// The returned `String` is owned by the caller, and it is the caller’s responsibility to clear
274/// it. This can be done via [`Zeroize`], e.g.:
275/// ```no_run
276/// # use readpassphrase_3::{
277/// #     PASSWORD_LEN,
278/// #     Error,
279/// #     Flags,
280/// #     readpassphrase_into,
281/// # };
282/// # use readpassphrase_3::Zeroize;
283/// # fn main() -> Result<(), Error> {
284/// let buf = vec![0u8; PASSWORD_LEN];
285/// let mut pass = readpassphrase_into(c"Pass: ", buf, Flags::default())?;
286/// _ = pass;
287/// pass.zeroize();
288/// # Ok(())
289/// # }
290/// ```
291pub fn readpassphrase_into(
292    prompt: &CStr,
293    mut buf: Vec<u8>,
294    flags: Flags,
295) -> Result<String, IntoError> {
296    let bufsiz = cmp::max(buf.len(), cmp::min(buf.capacity(), MAX_CAPACITY));
297    if cfg!(debug_assertions) {
298        // Fill `buf` with nonzero bytes to check that `ffi::readpassphrase` wrote a nul.
299        buf.fill(1);
300        buf.resize(bufsiz, 1);
301    } else {
302        buf.resize(bufsiz, 0);
303    }
304    let prompt = prompt.as_ptr();
305    let buf_ptr = buf.as_mut_ptr().cast();
306    let flags = flags.bits();
307    // SAFETY: By construction as with `readpassphrase` above.
308    let res = unsafe { ffi::readpassphrase(prompt, buf_ptr, bufsiz, flags) };
309    if res.is_null() {
310        buf.clear();
311        return Err(IntoError(io::Error::last_os_error().into(), Some(buf)));
312    }
313    let len = buf.iter().position(|&b| b == 0).unwrap();
314    buf.truncate(len);
315    match String::from_utf8(buf) {
316        Ok(s) => Ok(s),
317        Err(e) => {
318            let err = e.utf8_error();
319            let buf = e.into_bytes();
320            Err(IntoError(Error::Utf8(err), Some(buf)))
321        }
322    }
323}
324
325impl IntoError {
326    /// Return the [`Error`] corresponding to this.
327    pub fn error(&self) -> &Error {
328        &self.0
329    }
330
331    /// Returns the buffer that was passed to [`readpassphrase_into`].
332    ///
333    /// # Security
334    /// The returned buffer may contain sensitive data in its spare capacity, even if the
335    /// buffer’s length is zero. It is the caller’s responsibility to zero it as soon as possible
336    /// if needed, e.g. using [`Zeroize`]:
337    /// ```no_run
338    /// # use std::io::*;
339    /// # use readpassphrase_3::{Flags, Zeroize, readpassphrase_into};
340    /// # let err = readpassphrase_into(c"", vec![], Flags::empty()).unwrap_err();
341    /// let mut buf = err.into_bytes();
342    /// // ...
343    /// buf.zeroize();
344    /// ```
345    pub fn into_bytes(mut self) -> Vec<u8> {
346        self.1.take().unwrap()
347    }
348}
349
350impl error::Error for IntoError {
351    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
352        Some(&self.0)
353    }
354}
355
356impl fmt::Display for IntoError {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        self.0.fmt(f)
359    }
360}
361
362impl Drop for IntoError {
363    fn drop(&mut self) {
364        if let Some(mut buf) = self.1.take() {
365            buf.zeroize();
366        }
367    }
368}
369
370impl From<IntoError> for Error {
371    fn from(mut value: IntoError) -> Self {
372        mem::replace(&mut value.0, Error::Io(io::ErrorKind::Other.into()))
373    }
374}
375
376impl From<io::Error> for Error {
377    fn from(value: io::Error) -> Self {
378        Error::Io(value)
379    }
380}
381
382impl From<str::Utf8Error> for Error {
383    fn from(value: str::Utf8Error) -> Self {
384        Error::Utf8(value)
385    }
386}
387
388impl error::Error for Error {
389    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
390        Some(match self {
391            Error::Io(e) => e,
392            Error::Utf8(e) => e,
393        })
394    }
395}
396
397impl fmt::Display for Error {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        match self {
400            Error::Io(e) => e.fmt(f),
401            Error::Utf8(e) => e.fmt(f),
402        }
403    }
404}
405
406#[cfg(any(docsrs, not(feature = "zeroize")))]
407mod our_zeroize {
408    use std::{arch::asm, mem::MaybeUninit};
409
410    /// A minimal in-crate implementation of a subset of [`zeroize::Zeroize`].
411    ///
412    /// This provides compile-fenced memory zeroing for [`String`]s and [`Vec`]s without needing to
413    /// depend on the `zeroize` crate.
414    ///
415    /// If the optional `zeroize` feature is enabled, then the trait is replaced with a re-export of
416    /// `zeroize::Zeroize` itself.
417    pub trait Zeroize {
418        fn zeroize(&mut self);
419    }
420
421    impl Zeroize for Vec<u8> {
422        fn zeroize(&mut self) {
423            self.clear();
424            let buf = self.spare_capacity_mut();
425            buf.fill(MaybeUninit::zeroed());
426            compile_fence(buf);
427        }
428    }
429
430    impl Zeroize for String {
431        fn zeroize(&mut self) {
432            // SAFETY: we clear the string.
433            unsafe { self.as_mut_vec() }.zeroize();
434        }
435    }
436
437    impl Zeroize for [u8] {
438        fn zeroize(&mut self) {
439            self.fill(0);
440            compile_fence(self);
441        }
442    }
443
444    fn compile_fence<T>(buf: &[T]) {
445        unsafe {
446            asm!(
447                "/* {ptr} */",
448                ptr = in(reg) buf.as_ptr(),
449                options(nostack, preserves_flags, readonly)
450            );
451        }
452    }
453}
454
455#[cfg(use_tcm)]
456pub(crate) use tcm_readpassphrase_vendored as ffi;
457#[cfg(not(use_tcm))]
458mod ffi {
459    use std::ffi::{c_char, c_int};
460
461    extern "C" {
462        pub(crate) fn readpassphrase(
463            prompt: *const c_char,
464            buf: *mut c_char,
465            bufsiz: usize,
466            flags: c_int,
467        ) -> *mut c_char;
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn test_empty() {
477        let err = readpassphrase_into(c"pass", Vec::new(), Flags::empty()).unwrap_err();
478        let Error::Io(err) = err.into() else {
479            panic!();
480        };
481        #[cfg(not(windows))]
482        assert_eq!(io::ErrorKind::InvalidInput, err.kind());
483        #[cfg(windows)]
484        {
485            _ = err
486        };
487
488        let mut buf = Vec::new();
489        let err = readpassphrase(c"pass", &mut buf, Flags::empty()).unwrap_err();
490        let Error::Io(err) = err else {
491            panic!();
492        };
493        #[cfg(not(windows))]
494        assert_eq!(io::ErrorKind::InvalidInput, err.kind());
495        #[cfg(windows)]
496        {
497            _ = err
498        };
499    }
500
501    #[test]
502    fn test_zeroize() {
503        let mut buf = "test".to_string();
504        buf.zeroize();
505        unsafe { buf.as_mut_vec().set_len(4) };
506        assert_eq!("\0\0\0\0", &buf);
507        let mut buf = vec![1u8; 15];
508        unsafe { buf.set_len(0) };
509        let x = buf.spare_capacity_mut()[0];
510        assert_eq!(unsafe { x.assume_init() }, 1);
511        buf.zeroize();
512        unsafe { buf.set_len(15) };
513        assert_eq!(vec![0u8; 15], buf);
514        let mut buf = vec![1u8; 2];
515        unsafe { buf.set_len(1) };
516        let slice = &mut *buf;
517        slice.zeroize();
518        unsafe { buf.set_len(2) };
519        assert_eq!(vec![0u8, 1], buf);
520    }
521}