readpassphrase_3/
lib.rs

1// Copyright 2025
2//	Steven Dee
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions
6// are met:
7//
8// Redistributions of source code must retain the above copyright
9// notice, this list of conditions and the following disclaimer.
10//
11// THIS SOFTWARE IS PROVIDED BY STEVEN DEE “AS IS” AND ANY EXPRESS
12// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
13// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
14// ARE DISCLAIMED. IN NO EVENT SHALL STEVEN DEE BE LIABLE FOR ANY
15// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
16// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
17// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
18// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
19// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
20// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
21// IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
23//! This library endeavors to expose a thin wrapper around OpenBSD’s [`readpassphrase(3)`][0]
24//! function.
25//!
26//! Three different interfaces are exposed; for most purposes, you will want to use either
27//! [`getpass`] (for simple password entry) or [`readpassphrase`] (when you need flags from
28//! `readpassphrase(3)` or need more control over the memory.)
29//!
30//! The [`readpassphrase_owned`] function is a bit more niche; it may be used when you need a
31//! [`String`] output but need to pass flags or control the buffer size (vs [`getpass`].)
32//!
33//! [0]: https://man.openbsd.org/readpassphrase
34
35use std::{
36    ffi::{CStr, FromBytesUntilNulError},
37    fmt::Display,
38    io, mem,
39    str::Utf8Error,
40};
41
42use bitflags::bitflags;
43
44pub const PASSWORD_LEN: usize = 256;
45
46bitflags! {
47    /// Flags for controlling readpassphrase
48    pub struct RppFlags: i32 {
49        /// Turn off echo (default)
50        const ECHO_OFF    = 0x00;
51        /// Leave echo on
52        const ECHO_ON     = 0x01;
53        /// Fail if there is no tty
54        const REQUIRE_TTY = 0x02;
55        /// Force input to lower case
56        const FORCELOWER  = 0x04;
57        /// Force input to upper case
58        const FORCEUPPER  = 0x08;
59        /// Strip the high bit from input
60        const SEVENBIT    = 0x10;
61        /// Read from stdin, not /dev/tty
62        const STDIN       = 0x20;
63    }
64}
65
66impl Default for RppFlags {
67    fn default() -> Self {
68        Self::ECHO_OFF
69    }
70}
71
72/// Errors that can occur in readpassphrase
73#[derive(Debug)]
74pub enum Error {
75    /// `readpassphrase(3)` itself encountered an error
76    Io(io::Error),
77    /// The entered password did not parse as UTF-8
78    Utf8(Utf8Error),
79    /// The buffer did not contain a null terminator
80    CStr(FromBytesUntilNulError),
81}
82
83/// Reads a passphrase using `readpassphrase(3)`.
84///
85/// This function reads a passphrase of up to `buf.len() - 1` bytes. If the entered passphrase is
86/// longer, it will be truncated.
87///
88/// # Security
89/// The passed buffer might contain sensitive data, even if this function returns an error (for
90/// example, if the contents are not valid UTF-8.) It is often considered good practice to zero
91/// this memory after you’re done with it, for example by using [`zeroize`]:
92/// ```no_run
93/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase};
94/// use zeroize::Zeroizing;
95/// # fn main() -> Result<(), Error> {
96/// let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
97/// let pass = readpassphrase(c"Pass: ", &mut buf, RppFlags::default())?;
98/// # Ok(())
99/// # }
100/// ```
101pub fn readpassphrase<'a>(
102    prompt: &CStr,
103    buf: &'a mut [u8],
104    flags: RppFlags,
105) -> Result<&'a str, Error> {
106    unsafe {
107        let res = ffi::readpassphrase(
108            prompt.as_ptr(),
109            buf.as_mut_ptr().cast(),
110            buf.len(),
111            flags.bits(),
112        );
113        if res.is_null() {
114            return Err(io::Error::last_os_error().into());
115        }
116    }
117    Ok(CStr::from_bytes_until_nul(buf)?.to_str()?)
118}
119
120/// Reads a passphrase using `readpassphrase(3)`, returning it as a [`String`].
121///
122/// Internally, this function uses a buffer of [`PASSWORD_LEN`] bytes, allowing for passwords up to
123/// `PASSWORD_LEN - 1` characters (accounting for the C null terminator.) If the entered passphrase
124/// is longer, it will be truncated.
125///
126/// The passed flags are always the defaults, i.e., [`RppFlags::ECHO_OFF`].
127///
128/// # Security
129/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
130/// to clear it when you are done with it, for example by using [`zeroize`]:
131/// ```no_run
132/// # use readpassphrase_3::{Error, getpass};
133/// use zeroize::Zeroizing;
134/// # fn main() -> Result<(), Error> {
135/// let pass = Zeroizing::new(getpass(c"Pass: ")?);
136/// # Ok(())
137/// # }
138/// ```
139pub fn getpass(prompt: &CStr) -> Result<String, Error> {
140    #[allow(unused_mut, unused_variables)]
141    readpassphrase_owned(prompt, vec![0u8; PASSWORD_LEN], RppFlags::empty()).map_err(
142        |(e, mut buf)| {
143            explicit_bzero(&mut buf);
144            e
145        },
146    )
147}
148
149/// Reads a passphrase using `readpassphrase(3)` using the passed buffer’s memory.
150///
151/// This function reads a passphrase of up to `buf.capacity() - 1` bytes. If the entered passphrase
152/// is longer, it will be truncated.
153///
154/// The returned [`String`] uses `buf`’s memory; on failure, this memory is returned to the caller
155/// in the second argument of the `Err` tuple with its length set to 0.
156///
157/// # Security
158/// The returned `String` is owned by the caller, and it is the caller’s responsibility to clear
159/// it. It is also the caller’s responsibility to clear the buffer returned on error, as it may
160/// still contain sensitive data, for example if the password was not valid UTF-8.
161///
162/// This can be done via [`zeroize`], e.g.:
163/// ```no_run
164/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase_owned};
165/// use zeroize::{Zeroizing, Zeroize};
166/// # fn main() -> Result<(), Error> {
167/// let buf = vec![0u8; PASSWORD_LEN];
168/// let pass = Zeroizing::new(
169///     readpassphrase_owned(c"Pass: ", buf, RppFlags::default())
170///         .map_err(|(e, mut buf)| { buf.zeroize(); e })?
171/// );
172/// # Ok(())
173/// # }
174/// ```
175pub fn readpassphrase_owned(
176    prompt: &CStr,
177    mut buf: Vec<u8>,
178    flags: RppFlags,
179) -> Result<String, (Error, Vec<u8>)> {
180    readpassphrase_mut(prompt, &mut buf, flags).map_err(|e| {
181        buf.clear();
182        (e, buf)
183    })
184}
185
186/// Reads a passphrase into `buf`’s maybe-uninitialized capacity and returns it as a `String`
187/// reusing `buf`’s memory on success. This function serves to make it possible to write
188/// `readpassphrase_owned` without either pre-initializing the buffer or invoking undefined
189/// behavior by constructing a maybe-uninitialized slice.
190fn readpassphrase_mut(prompt: &CStr, buf: &mut Vec<u8>, flags: RppFlags) -> Result<String, Error> {
191    unsafe {
192        let res = ffi::readpassphrase(
193            prompt.as_ptr(),
194            buf.as_mut_ptr().cast(),
195            buf.capacity(),
196            flags.bits(),
197        );
198        if res.is_null() {
199            return Err(io::Error::last_os_error().into());
200        }
201        let res = CStr::from_ptr(res).to_str()?;
202        buf.set_len(res.len());
203        Ok(String::from_utf8_unchecked(mem::take(buf)))
204    }
205}
206
207/// Securely zero the memory in `buf`.
208///
209/// This function clears the full capacity of `buf` by writing zeroes to it, thereby erasing any
210/// sensitive data in `buf`. It should be called to clear any sensitive passphrases once they are
211/// no longer in use.
212///
213/// If the `zeroize` feature is enabled, this internally uses [`zeroize::Zeroize`].
214pub fn explicit_bzero(buf: &mut Vec<u8>) {
215    #[cfg(feature = "zeroize")]
216    {
217        use zeroize::Zeroize;
218        buf.zeroize();
219    }
220    #[cfg(not(feature = "zeroize"))]
221    {
222        buf.clear();
223        buf.spare_capacity_mut()
224            .fill(std::mem::MaybeUninit::zeroed());
225        unsafe {
226            core::arch::asm!(
227                "/* {ptr} */",
228                ptr = in(reg) buf.as_ptr(),
229                options(nostack, readonly, preserves_flags),
230            );
231        }
232    }
233}
234
235impl From<io::Error> for Error {
236    fn from(value: io::Error) -> Self {
237        Error::Io(value)
238    }
239}
240
241impl From<Utf8Error> for Error {
242    fn from(value: Utf8Error) -> Self {
243        Error::Utf8(value)
244    }
245}
246
247impl From<FromBytesUntilNulError> for Error {
248    fn from(value: FromBytesUntilNulError) -> Self {
249        Error::CStr(value)
250    }
251}
252
253impl core::error::Error for Error {
254    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
255        match self {
256            Error::Io(e) => Some(e),
257            Error::Utf8(e) => Some(e),
258            Error::CStr(e) => Some(e),
259        }
260    }
261}
262
263impl Display for Error {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        match self {
266            Error::Io(e) => e.fmt(f),
267            Error::Utf8(e) => e.fmt(f),
268            Error::CStr(e) => e.fmt(f),
269        }
270    }
271}
272
273mod ffi {
274    use std::ffi::{c_char, c_int};
275
276    unsafe extern "C" {
277        pub(crate) unsafe fn readpassphrase(
278            prompt: *const c_char,
279            buf: *mut c_char,
280            bufsiz: usize,
281            flags: c_int,
282        ) -> *mut c_char;
283    }
284}