readpassphrase_3/
lib.rs

1// Copyright 2025 Steven Dee.
2//
3// This project is made available under a BSD-compatible license. See the
4// LICENSE file in the project root for details.
5//
6// The readpassphrase source and header are copyright 2000-2002, 2007, 2010
7// Todd C. Miller.
8
9//! This library endeavors to expose a thin wrapper around OpenBSD’s [`readpassphrase(3)`][0]
10//! function.
11//!
12//! Three different interfaces are exposed; for most purposes, you will want to use either
13//! [`getpass`] (for simple password entry) or [`readpassphrase`] (when you need flags from
14//! `readpassphrase(3)` or need more control over the memory.)
15//!
16//! The [`readpassphrase_owned`] function is a bit more niche; it may be used when you need a
17//! [`String`] output but need to pass flags or control the buffer size (vs [`getpass`].)
18//!
19//! [0]: https://man.openbsd.org/readpassphrase
20
21use std::{
22    ffi::{CStr, FromBytesUntilNulError},
23    fmt::Display,
24    io,
25    str::Utf8Error,
26};
27
28use bitflags::bitflags;
29#[cfg(feature = "zeroize")]
30use zeroize::Zeroize;
31
32pub const PASSWORD_LEN: usize = 256;
33
34bitflags! {
35    /// Flags for controlling readpassphrase
36    pub struct RppFlags: i32 {
37        /// Turn off echo (default)
38        const ECHO_OFF    = 0x00;
39        /// Leave echo on
40        const ECHO_ON     = 0x01;
41        /// Fail if there is no tty
42        const REQUIRE_TTY = 0x02;
43        /// Force input to lower case
44        const FORCELOWER  = 0x04;
45        /// Force input to upper case
46        const FORCEUPPER  = 0x08;
47        /// Strip the high bit from input
48        const SEVENBIT    = 0x10;
49        /// Read from stdin, not /dev/tty
50        const STDIN       = 0x20;
51    }
52}
53
54impl Default for RppFlags {
55    fn default() -> Self {
56        Self::ECHO_OFF
57    }
58}
59
60/// Errors that can occur in readpassphrase
61#[derive(Debug)]
62pub enum Error {
63    /// `readpassphrase(3)` itself encountered an error
64    Io(io::Error),
65    /// The entered password did not parse as UTF-8
66    Utf8(Utf8Error),
67    /// The buffer did not contain a null terminator
68    CStr(FromBytesUntilNulError),
69}
70
71/// Reads a passphrase using `readpassphrase(3)`.
72///
73/// This function reads a passphrase of up to `buf.len() - 1` bytes. If the entered passphrase is
74/// longer, it will be truncated.
75///
76/// # Security
77/// The passed buffer might contain sensitive data, even if this function returns an error (for
78/// example, if the contents are not valid UTF-8.) It is often considered good practice to zero
79/// this memory after you’re done with it, for example by using [`zeroize`]:
80/// ```no_run
81/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase};
82/// use zeroize::Zeroizing;
83/// # fn main() -> Result<(), Error> {
84/// let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
85/// let pass = readpassphrase(c"Pass: ", &mut buf, RppFlags::default())?;
86/// # Ok(())
87/// # }
88/// ```
89pub fn readpassphrase<'a>(
90    prompt: &CStr,
91    buf: &'a mut [u8],
92    flags: RppFlags,
93) -> Result<&'a str, Error> {
94    unsafe {
95        let res = ffi::readpassphrase(
96            prompt.as_ptr(),
97            buf.as_mut_ptr().cast(),
98            buf.len(),
99            flags.bits(),
100        );
101        if res.is_null() {
102            return Err(io::Error::last_os_error().into());
103        }
104    }
105    Ok(CStr::from_bytes_until_nul(buf)?.to_str()?)
106}
107
108/// Reads a passphrase using `readpassphrase(3)`, returning it as a [`String`].
109///
110/// Internally, this function uses a buffer of [`PASSWORD_LEN`] bytes, allowing for passwords up to
111/// `PASSWORD_LEN - 1` characters (accounting for the C null terminator.) If the entered passphrase
112/// is longer, it will be truncated.
113///
114/// The passed flags are always the defaults, i.e., [`RppFlags::ECHO_OFF`].
115///
116/// # Security
117/// If the [`zeroize`] feature of this crate is disabled, then this function can leak sensitive
118/// data on failure, e.g. if the entered passphrase is not valid UTF-8. There is no way around this
119/// (other than using the default `zeroize` feature), so if you must turn that feature off and are
120/// concerned about this, then you should use the [`readpassphrase`] function instead.
121///
122/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
123/// to clear it when you are done with it, for example by using [`zeroize`]:
124/// ```no_run
125/// # use readpassphrase_3::{Error, getpass};
126/// use zeroize::Zeroizing;
127/// # fn main() -> Result<(), Error> {
128/// let pass = Zeroizing::new(getpass(c"Pass: ")?);
129/// # Ok(())
130/// # }
131/// ```
132pub fn getpass(prompt: &CStr) -> Result<String, Error> {
133    #[allow(unused_mut, unused_variables)]
134    readpassphrase_owned(prompt, vec![0u8; PASSWORD_LEN], RppFlags::empty()).map_err(
135        |(e, mut buf)| {
136            #[cfg(feature = "zeroize")]
137            buf.zeroize();
138            e
139        },
140    )
141}
142
143/// Reads a passphrase using `readpassphrase(3)` using the passed buffer’s memory.
144///
145/// This function reads a passphrase of up to `buf.len() - 1` bytes. If the entered passphrase is
146/// longer, it will be truncated.
147///
148/// The returned [`String`] uses `buf`’s memory; on failure, this memory is returned to the caller in
149/// the second argument of the `Err` tuple.
150///
151/// # Security
152/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
153/// to clear it when you are done with it. You may also wish to zero the returned buffer on error,
154/// as it may still contain sensitive data, for example if the password was not valid UTF-8.
155///
156/// This can be done via [`zeroize`], e.g.:
157/// ```no_run
158/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase_owned};
159/// use zeroize::{Zeroizing, Zeroize};
160/// # fn main() -> Result<(), Error> {
161/// let buf = vec![0u8; PASSWORD_LEN];
162/// let pass = Zeroizing::new(
163///     readpassphrase_owned(c"Pass: ", buf, RppFlags::default())
164///         .map_err(|(e, mut buf)| { buf.zeroize(); e })?
165/// );
166/// # Ok(())
167/// # }
168/// ```
169pub fn readpassphrase_owned(
170    prompt: &CStr,
171    mut buf: Vec<u8>,
172    flags: RppFlags,
173) -> Result<String, (Error, Vec<u8>)> {
174    match readpassphrase(prompt, &mut buf, flags) {
175        Ok(res) => {
176            let len = res.len();
177            buf.truncate(len);
178            Ok(unsafe { String::from_utf8_unchecked(buf) })
179        }
180
181        Err(e) => Err((e, buf)),
182    }
183}
184
185impl From<io::Error> for Error {
186    fn from(value: io::Error) -> Self {
187        Error::Io(value)
188    }
189}
190
191impl From<Utf8Error> for Error {
192    fn from(value: Utf8Error) -> Self {
193        Error::Utf8(value)
194    }
195}
196
197impl From<FromBytesUntilNulError> for Error {
198    fn from(value: FromBytesUntilNulError) -> Self {
199        Error::CStr(value)
200    }
201}
202
203impl core::error::Error for Error {
204    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
205        match self {
206            Error::Io(e) => Some(e),
207            Error::Utf8(e) => Some(e),
208            Error::CStr(e) => Some(e),
209        }
210    }
211}
212
213impl Display for Error {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        match self {
216            Error::Io(e) => e.fmt(f),
217            Error::Utf8(e) => e.fmt(f),
218            Error::CStr(e) => e.fmt(f),
219        }
220    }
221}
222
223mod ffi {
224    use std::ffi::{c_char, c_int};
225
226    unsafe extern "C" {
227        pub(crate) unsafe fn readpassphrase(
228            prompt: *const c_char,
229            buf: *mut c_char,
230            bufsiz: usize,
231            flags: c_int,
232        ) -> *mut c_char;
233    }
234}