readpassphrase_3/
lib.rs

1// Copyright 2025
2//	Steven Dee
3//
4// Permission to use, copy, modify, and/or distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
10// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,
11// OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
12// DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
13// ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
15//! Lightweight, easy-to-use wrapper around the C [`readpassphrase(3)`][0] function.
16//!
17//! From the man page:
18//! > The `readpassphrase()` function displays a prompt to, and reads in a passphrase from,
19//! > `/dev/tty`. If this file is inaccessible and the [`RPP_REQUIRE_TTY`](Flags::REQUIRE_TTY) flag
20//! > is not set, `readpassphrase()` displays the prompt on the standard error output and reads
21//! > from the standard input.
22//!
23//! # Usage
24//! For the simplest of cases, where you would just like to read a password from the console into a
25//! [`String`] to use elsewhere, you can use [`getpass`]:
26//! ```no_run
27//! use readpassphrase_3::getpass;
28//! let _ = getpass(c"Enter your password: ").expect("failed reading password");
29//! ```
30//!
31//! If you need to pass [`Flags`] or to control the buffer size, then you can use
32//! [`readpassphrase`] or [`readpassphrase_into`] depending on your ownership requirements:
33//! ```no_run
34//! let mut buf = vec![0u8; 256];
35//! use readpassphrase_3::{Flags, readpassphrase};
36//! let pass: &str = readpassphrase(c"Password: ", &mut buf, Flags::default()).unwrap();
37//!
38//! use readpassphrase_3::readpassphrase_into;
39//! let pass: String = readpassphrase_into(c"Pass: ", buf, Flags::FORCELOWER).unwrap();
40//! # _ = pass;
41//! ```
42//!
43//! # Security
44//! The [`readpassphrase(3)` man page][0] says:
45//! > The calling process should zero the passphrase as soon as possible to avoid leaving the
46//! > cleartext passphrase visible in the process's address space.
47//!
48//! It is your job to ensure that this is done with the data you own, i.e.
49//! any [`Vec`] passed to [`readpassphrase`] or any [`String`] received from [`getpass`] or
50//! [`readpassphrase_into`].
51//!
52//! This crate ships with a minimal [`Zeroize`] trait that may be used for this purpose:
53//! ```no_run
54//! # use readpassphrase_3::{Flags, getpass, readpassphrase, readpassphrase_into};
55//! use readpassphrase_3::Zeroize;
56//! let mut pass = getpass(c"password: ").unwrap();
57//! // do_something_with(&pass);
58//! pass.zeroize();
59//!
60//! let mut buf = vec![0u8; 256];
61//! let res = readpassphrase(c"password: ", &mut buf, Flags::empty());
62//! // match_something_on(res);
63//! buf.zeroize();
64//!
65//! let mut pass = readpassphrase_into(c"password: ", buf, Flags::empty()).unwrap();
66//! // do_something_with(&pass);
67//! pass.zeroize();
68//! ```
69//!
70//! ## Zeroizing memory
71//! This crate works well with the [`zeroize`] crate. For example, [`zeroize::Zeroizing`] may be
72//! used to zero buffer contents regardless of a function’s control flow:
73//! ```no_run
74//! # use readpassphrase_3::{Error, Flags, PASSWORD_LEN, getpass, readpassphrase};
75//! use zeroize::Zeroizing;
76//! # fn main() -> Result<(), Error> {
77//! let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
78//! let pass = readpassphrase(c"pass: ", &mut buf, Flags::REQUIRE_TTY)?;
79//! // do_something_that_can_fail_with(pass)?;
80//!
81//! // Or alternatively:
82//! let pass = Zeroizing::new(getpass(c"pass: ")?);
83//! // do_something_that_can_fail_with(&pass)?;
84//! # Ok(())
85//! # }
86//! ```
87//!
88//! If this crate’s `zeroize` feature is enabled, then its [`Zeroize`] will be replaced by a
89//! re-export of the upstream [`zeroize::Zeroize`].
90//!
91//! # “Mismatched types” errors
92//! The prompt strings in this API are <code>&[CStr]</code>, not <code>&[str]</code>.
93//! This is because the underlying C function assumes that the prompt is a null-terminated string;
94//! were we to take `&str` instead of `&CStr`, we would need to make a copy of the prompt on every
95//! call.
96//!
97//! Most of the time, your prompts will be string literals; you can ask Rust to give you a `&CStr`
98//! literal by simply prepending `c` to the string:
99//! ```no_run
100//! # use readpassphrase_3::{Error, getpass};
101//! # fn main() -> Result<(), Error> {
102//! let _ = getpass(c"pass: ")?;
103//! //              ^
104//! //              |
105//! //              like this
106//! # Ok(())
107//! # }
108//! ```
109//!
110//! If you need a dynamic prompt, look at [`CString`](std::ffi::CString).
111//!
112//! # Windows Limitations
113//! The Windows implementation of `readpassphrase(3)` that we are using does not yet support UTF-8
114//! in prompts; they must be ASCII. It also does not yet support flags, and always behaves as
115//! though called with [`Flags::empty()`].
116//!
117//! [0]: https://man.openbsd.org/readpassphrase
118//! [str]: prim@str "str"
119
120use std::{error, ffi::CStr, fmt, io, mem, str};
121
122use bitflags::bitflags;
123#[cfg(any(docsrs, not(feature = "zeroize")))]
124pub use our_zeroize::Zeroize;
125#[cfg(all(not(docsrs), feature = "zeroize"))]
126pub use zeroize::Zeroize;
127
128/// Size of buffer used in [`getpass`].
129///
130/// Because `readpassphrase(3)` null-terminates its string, the actual maximum password length for
131/// [`getpass`] is 255.
132pub const PASSWORD_LEN: usize = 256;
133
134bitflags! {
135    /// Flags for controlling readpassphrase.
136    ///
137    /// The default flag `ECHO_OFF` is not represented here because `bitflags` [recommends against
138    /// zero-bit flags][0]; it may be specified as either [`Flags::empty()`] or
139    /// [`Flags::default()`].
140    ///
141    /// Note that the Windows `readpassphrase(3)` implementation always acts like it has been
142    /// passed `ECHO_OFF`, i.e., the flags are ignored.
143    ///
144    /// [0]: https://docs.rs/bitflags/latest/bitflags/#zero-bit-flags
145    #[derive(Default)]
146    pub struct Flags: i32 {
147        /// Leave echo on.
148        const ECHO_ON     = 0x01;
149        /// Fail if there is no tty.
150        const REQUIRE_TTY = 0x02;
151        /// Force input to lower case.
152        const FORCELOWER  = 0x04;
153        /// Force input to upper case.
154        const FORCEUPPER  = 0x08;
155        /// Strip the high bit from input.
156        const SEVENBIT    = 0x10;
157        /// Read from stdin, not `/dev/tty`.
158        const STDIN       = 0x20;
159    }
160}
161
162/// Errors that can occur in readpassphrase.
163#[derive(Debug)]
164pub enum Error {
165    /// `readpassphrase(3)` itself encountered an error.
166    Io(io::Error),
167    /// The entered password was not UTF-8.
168    Utf8(str::Utf8Error),
169}
170
171/// Reads a passphrase using `readpassphrase(3)`.
172///
173/// This function returns a <code>&[str]</code> backed by `buf`, representing a password of up to
174/// `buf.len() - 1` bytes. Any additional characters and the terminating newline are discarded.
175///
176/// # Errors
177/// Returns [`Err`] if `readpassphrase(3)` itself failed or if the entered password is not UTF-8.
178/// The former will be represented by [`Error::Io`] and the latter by [`Error::Utf8`].
179///
180/// # Security
181/// The passed buffer might contain sensitive data, even if this function returns an error.
182/// Therefore it should be zeroed as soon as possible. This can be achieved, for example, with
183/// [`zeroize::Zeroizing`]:
184/// ```no_run
185/// # use readpassphrase_3::{PASSWORD_LEN, Error, Flags, readpassphrase};
186/// use zeroize::Zeroizing;
187/// # fn main() -> Result<(), Error> {
188/// let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
189/// let pass = readpassphrase(c"Pass: ", &mut buf, Flags::default())?;
190/// # Ok(())
191/// # }
192/// ```
193/// [str]: prim@str "str"
194pub fn readpassphrase<'a>(
195    prompt: &CStr,
196    buf: &'a mut [u8],
197    flags: Flags,
198) -> Result<&'a str, Error> {
199    let prompt = prompt.as_ptr();
200    let buf_ptr = buf.as_mut_ptr().cast();
201    let bufsiz = buf.len();
202    let flags = flags.bits();
203    // SAFETY: `prompt` is a nul-terminated byte sequence, and `buf_ptr` is an allocation of at
204    // least `bufsiz` bytes, as guaranteed by `&CStr` and `&mut [u8]` respectively.
205    let res = unsafe { ffi::readpassphrase(prompt, buf_ptr, bufsiz, flags) };
206    if res.is_null() {
207        return Err(io::Error::last_os_error().into());
208    }
209    Ok(CStr::from_bytes_until_nul(buf).unwrap().to_str()?)
210}
211
212/// Reads a passphrase using `readpassphrase(3)`, returning a [`String`].
213///
214/// Internally, this function uses a buffer of [`PASSWORD_LEN`] bytes, allowing for passwords up to
215/// `PASSWORD_LEN - 1` characters (accounting for the C null terminator.) If the entered passphrase
216/// is longer, it will be truncated to the maximum length.
217///
218/// # Errors
219/// Returns [`Err`] if `readpassphrase(3)` itself failed or if the entered password is not UTF-8.
220/// The former will be represented by [`Error::Io`] and the latter by [`Error::Utf8`].
221///
222/// # Security
223/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
224/// to clear it when you are done with it:
225/// ```no_run
226/// # use readpassphrase_3::{Error, Zeroize, getpass};
227/// # fn main() -> Result<(), Error> {
228/// let mut pass = getpass(c"Pass: ")?;
229/// _ = pass;
230/// pass.zeroize();
231/// # Ok(())
232/// # }
233/// ```
234pub fn getpass(prompt: &CStr) -> Result<String, Error> {
235    let buf = Vec::with_capacity(PASSWORD_LEN);
236    Ok(readpassphrase_into(prompt, buf, Flags::empty())?)
237}
238
239/// An [`Error`] from [`readpassphrase_into`] containing the passed buffer.
240///
241/// The buffer is accessible via [`IntoError::into_bytes`][0], and the `Error` via
242/// [`IntoError::error`].
243///
244/// If [`into_bytes`][0] is not called, the buffer is automatically zeroed on drop.
245///
246/// This struct is also exported as [`OwnedError`]. That name is deprecated; please transition to
247/// using `IntoError` instead.
248///
249/// [0]: IntoError::into_bytes
250#[derive(Debug)]
251pub struct IntoError(Error, Option<Vec<u8>>);
252
253/// Reads a passphrase using `readpassphrase(3)`, returning `buf` as a [`String`].
254///
255/// This function reads a passphrase of up to `buf.capacity() - 1` bytes. If the entered passphrase
256/// is longer, it will be truncated.
257///
258/// The returned [`String`] reuses `buf`’s memory; no copies are made.
259///
260/// **NB**. Sometimes in Rust the capacity of a vector may be larger than you expect; if you need a
261/// precise limit on the length of the entered password, either use [`readpassphrase`] or truncate
262/// the returned string.
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.
268///
269/// See the docs for [`IntoError`] for more details on what you can do with this error.
270///
271/// # Security
272/// The returned `String` is owned by the caller, and it is the caller’s responsibility to clear
273/// it. This can be done via [`Zeroize`], e.g.:
274/// ```no_run
275/// # use readpassphrase_3::{
276/// #     PASSWORD_LEN,
277/// #     Error,
278/// #     Flags,
279/// #     readpassphrase_into,
280/// # };
281/// # use readpassphrase_3::Zeroize;
282/// # fn main() -> Result<(), Error> {
283/// let buf = vec![0u8; PASSWORD_LEN];
284/// let mut pass = readpassphrase_into(c"Pass: ", buf, Flags::default())?;
285/// _ = pass;
286/// pass.zeroize();
287/// # Ok(())
288/// # }
289/// ```
290pub fn readpassphrase_into(
291    prompt: &CStr,
292    mut buf: Vec<u8>,
293    flags: Flags,
294) -> Result<String, IntoError> {
295    let prompt = prompt.as_ptr();
296    let buf_ptr = buf.as_mut_ptr().cast();
297    let bufsiz = buf.capacity();
298    let flags = flags.bits();
299    // SAFETY: `prompt` from `&CStr` as above. `buf_ptr` points to an allocation of `bufsiz` bytes.
300    let res = unsafe { ffi::readpassphrase(prompt, buf_ptr, bufsiz, flags) };
301    if res.is_null() {
302        buf.clear();
303        return Err(IntoError(io::Error::last_os_error().into(), Some(buf)));
304    }
305    let nul_pos = (0..bufsiz as isize)
306        // SAFETY: `i` is within `bufsiz`, which is the size of `buf`’s allocation;
307        // `ffi::readpassphrase` initialized `buf` up through a zero byte. We scan `buf` in order;
308        // the zero byte we find is at or before the end of the initialized portion.
309        .position(|i| unsafe { *buf_ptr.offset(i) == 0 })
310        .unwrap();
311    // SAFETY: `buf` is initialized at least up to `nul_pos`.
312    unsafe { buf.set_len(nul_pos) };
313    String::from_utf8(buf).map_err(|err| {
314        let res = err.utf8_error();
315        IntoError(res.into(), Some(err.into_bytes()))
316    })
317}
318
319#[deprecated(since = "0.10.0", note = "please use `IntoError`")]
320pub use IntoError as OwnedError;
321
322/// Deprecated alias for [`readpassphrase_into`].
323#[deprecated(since = "0.10.0", note = "please use `readpassphrase_into`")]
324pub fn readpassphrase_owned(
325    prompt: &CStr,
326    buf: Vec<u8>,
327    flags: Flags,
328) -> Result<String, IntoError> {
329    readpassphrase_into(prompt, buf, flags)
330}
331
332impl IntoError {
333    /// Return the [`Error`] corresponding to this.
334    pub fn error(&self) -> &Error {
335        &self.0
336    }
337
338    /// Returns the buffer that was passed to [`readpassphrase_into`].
339    ///
340    /// # Panics
341    /// Panics if [`IntoError::take`] was called before this.
342    pub fn into_bytes(mut self) -> Vec<u8> {
343        self.1.take().unwrap()
344    }
345
346    /// Returns the buffer that was passed to [`readpassphrase_into`].
347    ///
348    /// If called multiple times, returns [`Vec::new`].
349    #[deprecated(since = "0.10.0", note = "please use `into_bytes` instead")]
350    pub fn take(&mut self) -> Vec<u8> {
351        self.1.take().unwrap_or_default()
352    }
353}
354
355impl error::Error for IntoError {
356    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
357        Some(&self.0)
358    }
359}
360
361impl fmt::Display for IntoError {
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        self.0.fmt(f)
364    }
365}
366
367impl Drop for IntoError {
368    fn drop(&mut self) {
369        self.1.take().as_mut().map(Zeroize::zeroize);
370    }
371}
372
373impl From<IntoError> for Error {
374    fn from(mut value: IntoError) -> Self {
375        mem::replace(&mut value.0, Error::Io(io::ErrorKind::Other.into()))
376    }
377}
378
379impl From<io::Error> for Error {
380    fn from(value: io::Error) -> Self {
381        Error::Io(value)
382    }
383}
384
385impl From<str::Utf8Error> for Error {
386    fn from(value: str::Utf8Error) -> Self {
387        Error::Utf8(value)
388    }
389}
390
391impl error::Error for Error {
392    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
393        Some(match self {
394            Error::Io(e) => e,
395            Error::Utf8(e) => e,
396        })
397    }
398}
399
400impl fmt::Display for Error {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        match self {
403            Error::Io(e) => e.fmt(f),
404            Error::Utf8(e) => e.fmt(f),
405        }
406    }
407}
408
409#[cfg(any(docsrs, not(feature = "zeroize")))]
410mod our_zeroize {
411    use std::{arch::asm, mem::MaybeUninit};
412
413    /// A minimal in-crate implementation of a subset of [`zeroize::Zeroize`].
414    ///
415    /// This provides compile-fenced memory zeroing for [`String`]s and [`Vec`]s without needing to
416    /// depend on the `zeroize` crate.
417    ///
418    /// If the optional `zeroize` feature is enabled, then the trait is replaced with a re-export of
419    /// `zeroize::Zeroize` itself.
420    pub trait Zeroize {
421        fn zeroize(&mut self);
422    }
423
424    impl Zeroize for Vec<u8> {
425        fn zeroize(&mut self) {
426            self.clear();
427            let buf = self.spare_capacity_mut();
428            buf.fill(MaybeUninit::zeroed());
429            compile_fence(buf);
430        }
431    }
432
433    impl Zeroize for String {
434        fn zeroize(&mut self) {
435            // SAFETY: we clear the string.
436            unsafe { self.as_mut_vec() }.zeroize();
437        }
438    }
439
440    impl Zeroize for [u8] {
441        fn zeroize(&mut self) {
442            self.fill(0);
443            compile_fence(self);
444        }
445    }
446
447    fn compile_fence<T>(buf: &[T]) {
448        unsafe {
449            asm!(
450                "/* {ptr} */",
451                ptr = in(reg) buf.as_ptr(),
452                options(nostack, preserves_flags, readonly)
453            );
454        }
455    }
456}
457
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}