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 the C [`readpassphrase(3)`][0] function.
24//!
25//! Three different interfaces are exposed; for most purposes, you will want to use either
26//! [`getpass`] (for simple password entry) or [`readpassphrase`] (when you need flags from
27//! `readpassphrase(3)` or need more control over the memory.)
28//!
29//! The [`readpassphrase_owned`] function is a bit more niche; it may be used when you need a
30//! [`String`] output but need to pass flags or control the buffer size (vs [`getpass`].)
31//!
32//! Sensitive data should be zeroed as soon as possible to avoid leaving it visible in the
33//! process’s address space.
34//!
35//! # Usage
36//! To read a passphrase from the console:
37//! ```no_run
38//! # use readpassphrase_3::{getpass, zeroize::Zeroize};
39//! let mut pass = getpass(c"password: ").unwrap();
40//! // do_something_with(&pass);
41//! pass.zeroize();
42//! ```
43//!
44//! To control the buffer size or (on non-Windows) flags:
45//! ```no_run
46//! # use readpassphrase_3::{RppFlags, readpassphrase};
47//! # let mut buf = vec![0u8; 1];
48//! let pass = readpassphrase(c"pass: ", &mut buf, RppFlags::ECHO_ON).unwrap();
49//! ```
50//!
51//! To do so while transferring ownership:
52//! ```no_run
53//! # use readpassphrase_3::{Error, RppFlags, readpassphrase_owned};
54//! # fn main() -> Result<(), Error> {
55//! # let buf = vec![0u8; 1];
56//! let pass = readpassphrase_owned(c"pass: ", buf, RppFlags::empty())?;
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! This crate works well with the [`zeroize`][1] crate; for example, [`zeroize::Zeroizing`][2] may
62//! be used to zero buffer contents regardless of a function’s control flow:
63//!
64//! ```no_run
65//! # use readpassphrase_3::{Error, PASSWORD_LEN, RppFlags, readpassphrase};
66//! use zeroize::Zeroizing;
67//! # fn main() -> Result<(), Error> {
68//! let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
69//! let pass = readpassphrase(c"pass: ", &mut buf, RppFlags::REQUIRE_TTY)?;
70//! // do_something_that_can_fail_with(pass)?;
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! # “Mismatched types” errors
76//! The prompt strings in this API are references to [CStr], not [str]. This is because the
77//! underlying C function assumes that the prompt is a null-terminated string; were we to take
78//! `&str` instead of `&CStr`, we would need to make a copy of the prompt on every call.
79//!
80//! Most of the time, your prompts will be string literals; you can ask Rust to give you a `&CStr`
81//! literal by simply prepending `c` to the string:
82//! ```no_run
83//! # use readpassphrase_3::{Error, getpass};
84//! # fn main() -> Result<(), Error> {
85//! let _ = getpass(c"pass: ")?;
86//! //              ^
87//! //              |
88//! //              like this
89//! # Ok(())
90//! # }
91//! ```
92//!
93//! # Windows Limitations
94//! The Windows implementation of `readpassphrase(3)` that we are using does not yet support UTF-8
95//! in prompts; they must be ASCII. It also does not yet support flags, and always behaves as
96//! though called with [`RppFlags::empty()`].
97//!
98//! [0]: https://man.openbsd.org/readpassphrase
99//! [1]: https://docs.rs/zeroize/latest/zeroize/
100//! [2]: https://docs.rs/zeroize/latest/zeroize/struct.Zeroizing.html
101
102use std::{ffi::CStr, fmt::Display, io, mem, str::Utf8Error};
103
104use bitflags::bitflags;
105use zeroize::Zeroize;
106
107#[cfg(all(not(docsrs), feature = "zeroize"))]
108pub use zeroize;
109
110/// Size of buffer used in [`getpass`].
111///
112/// Because `readpassphrase(3)` null-terminates its string, the actual maximum password length for
113/// [`getpass`] is 255.
114pub const PASSWORD_LEN: usize = 256;
115
116bitflags! {
117    /// Flags for controlling readpassphrase.
118    ///
119    /// The default flag `ECHO_OFF` is not represented here because `bitflags` [recommends against
120    /// zero-bit flags][0]; it may be specified as either [`RppFlags::empty()`] or
121    /// [`RppFlags::default()`].
122    ///
123    /// Note that the Windows `readpassphrase(3)` implementation always acts like it has been
124    /// passed `ECHO_OFF`, i.e., the flags are ignored.
125    ///
126    /// [0]: https://docs.rs/bitflags/latest/bitflags/#zero-bit-flags
127    #[derive(Default)]
128    pub struct RppFlags: i32 {
129        /// Leave echo on.
130        const ECHO_ON     = 0x01;
131        /// Fail if there is no tty.
132        const REQUIRE_TTY = 0x02;
133        /// Force input to lower case.
134        const FORCELOWER  = 0x04;
135        /// Force input to upper case.
136        const FORCEUPPER  = 0x08;
137        /// Strip the high bit from input.
138        const SEVENBIT    = 0x10;
139        /// Read from stdin, not `/dev/tty`.
140        const STDIN       = 0x20;
141    }
142}
143
144/// Errors that can occur in readpassphrase.
145#[derive(Debug)]
146pub enum Error {
147    /// `readpassphrase(3)` itself encountered an error.
148    Io(io::Error),
149    /// The entered password was not UTF-8.
150    Utf8(Utf8Error),
151}
152
153/// Reads a passphrase using `readpassphrase(3)`.
154///
155/// This function tries to faithfully wrap `readpassphrase(3)` without overhead; the only
156/// additional work it does is:
157/// 1. It converts from a Rust byte slice to a C pointer/length pair going in.
158/// 2. It converts from a C `char *` to a Rust UTF-8 `&str` coming out.
159/// 3. It translates errors from `errno` (or string conversion) into [`Result`].
160///
161/// This function reads a passphrase of up to `buf.len() - 1` bytes. If the entered passphrase is
162/// longer, it will be truncated.
163///
164/// # Security
165/// The passed buffer might contain sensitive data even if this function returns an error (for
166/// example, if the contents are not valid UTF-8.) Therefore it should be zeroed as soon as
167/// possible. This can be achieved, for example, with [`zeroize::Zeroizing`][0]:
168/// ```no_run
169/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase};
170/// use zeroize::Zeroizing;
171/// # fn main() -> Result<(), Error> {
172/// let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
173/// let pass = readpassphrase(c"Pass: ", &mut buf, RppFlags::default())?;
174/// # Ok(())
175/// # }
176/// ```
177///
178/// [0]: https://docs.rs/zeroize/latest/zeroize/struct.Zeroizing.html
179pub fn readpassphrase<'a>(
180    prompt: &CStr,
181    buf: &'a mut [u8],
182    flags: RppFlags,
183) -> Result<&'a str, Error> {
184    unsafe {
185        let res = ffi::readpassphrase(
186            prompt.as_ptr(),
187            buf.as_mut_ptr().cast(),
188            buf.len(),
189            flags.bits(),
190        );
191        if res.is_null() {
192            return Err(io::Error::last_os_error().into());
193        }
194    }
195    Ok(CStr::from_bytes_until_nul(buf).unwrap().to_str()?)
196}
197
198/// Reads a passphrase using `readpassphrase(3)`, returning it as a [`String`].
199///
200/// Internally, this function uses a buffer of [`PASSWORD_LEN`] bytes, allowing for passwords up to
201/// `PASSWORD_LEN - 1` characters (accounting for the C null terminator.) If the entered passphrase
202/// is longer, it will be truncated.
203///
204/// The passed flags are always [`RppFlags::default()`], i.e. `ECHO_OFF`.
205///
206/// # Security
207/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
208/// to clear it when you are done with it:
209/// ```no_run
210/// # use readpassphrase_3::{Error, getpass, zeroize::Zeroize};
211/// # fn main() -> Result<(), Error> {
212/// let mut pass = getpass(c"Pass: ")?;
213/// _ = pass;
214/// pass.zeroize();
215/// # Ok(())
216/// # }
217/// ```
218pub fn getpass(prompt: &CStr) -> Result<String, Error> {
219    Ok(readpassphrase_owned(
220        prompt,
221        vec![0u8; PASSWORD_LEN],
222        RppFlags::empty(),
223    )?)
224}
225
226/// An error from [`readpassphrase_owned`]. Contains the passed buffer.
227#[derive(Debug)]
228pub struct OwnedError(Error, Option<Vec<u8>>);
229
230/// Reads a passphrase using `readpassphrase(3)` by reusing the passed buffer’s memory.
231///
232/// This function reads a passphrase of up to `buf.capacity() - 1` bytes. If the entered passphrase
233/// is longer, it will be truncated.
234///
235/// The returned [`String`] uses `buf`’s memory; on failure, this memory is returned to the caller
236/// in the second argument of the `Err` tuple with its length set to 0.
237///
238/// # Security
239/// The returned `String` is owned by the caller, and it is the caller’s responsibility to clear
240/// it. It is also the caller’s responsibility to clear the buffer returned on error, as it may
241/// still contain sensitive data, for example if the password was not valid UTF-8.
242///
243/// This can be done via [`zeroize`], e.g.:
244/// ```no_run
245/// # use readpassphrase_3::{
246/// #     PASSWORD_LEN,
247/// #     Error,
248/// #     RppFlags,
249/// #     readpassphrase_owned,
250/// #     zeroize::Zeroize,
251/// # };
252/// # fn main() -> Result<(), Error> {
253/// let buf = vec![0u8; PASSWORD_LEN];
254/// let mut pass = readpassphrase_owned(c"Pass: ", buf, RppFlags::default())?;
255/// _ = pass;
256/// pass.zeroize();
257/// # Ok(())
258/// # }
259/// ```
260pub fn readpassphrase_owned(
261    prompt: &CStr,
262    mut buf: Vec<u8>,
263    flags: RppFlags,
264) -> Result<String, OwnedError> {
265    readpassphrase_mut(prompt, &mut buf, flags).map_err(|e| {
266        buf.clear();
267        OwnedError(e, Some(buf))
268    })
269}
270
271// Reads a passphrase into `buf`’s maybe-uninitialized capacity and returns it as a `String`
272// reusing `buf`’s memory on success. This function serves to make it possible to write
273// `readpassphrase_owned` without either pre-initializing the buffer or invoking undefined
274// behavior by constructing a maybe-uninitialized slice.
275fn readpassphrase_mut(prompt: &CStr, buf: &mut Vec<u8>, flags: RppFlags) -> Result<String, Error> {
276    unsafe {
277        let res = ffi::readpassphrase(
278            prompt.as_ptr(),
279            buf.as_mut_ptr().cast(),
280            buf.capacity(),
281            flags.bits(),
282        );
283        if res.is_null() {
284            return Err(io::Error::last_os_error().into());
285        }
286        let res = CStr::from_ptr(res).to_str()?;
287        buf.set_len(res.len());
288        Ok(String::from_utf8_unchecked(mem::take(buf)))
289    }
290}
291
292/// Securely zero the memory in `buf`.
293///
294/// This function zeroes the full capacity of `buf`, erasing any sensitive data in it. It is
295/// a simple shim for [`zeroize`] and the latter should be used instead.
296///
297/// # Usage
298/// The following are equivalent:
299/// ```no_run
300/// # use readpassphrase_3::{explicit_bzero, zeroize::Zeroize};
301/// let mut buf = vec![1u8; 1];
302/// // 1.
303/// explicit_bzero(&mut buf);
304/// // 2.
305/// buf.zeroize();
306/// ```
307#[deprecated(since = "0.6.0", note = "use zeroize::Zeroize instead")]
308pub fn explicit_bzero(buf: &mut Vec<u8>) {
309    buf.zeroize();
310}
311
312impl OwnedError {
313    /// Take `buf` out of the error.
314    ///
315    /// Returns empty [`Vec`] after the first call.
316    pub fn take(&mut self) -> Vec<u8> {
317        self.1.take().unwrap_or_default()
318    }
319}
320
321impl Drop for OwnedError {
322    fn drop(&mut self) {
323        self.1.take().as_mut().map(zeroize::Zeroize::zeroize);
324    }
325}
326
327impl From<OwnedError> for Error {
328    fn from(mut value: OwnedError) -> Self {
329        mem::replace(&mut value.0, Error::Io(io::ErrorKind::Other.into()))
330    }
331}
332
333impl From<io::Error> for Error {
334    fn from(value: io::Error) -> Self {
335        Error::Io(value)
336    }
337}
338
339impl From<Utf8Error> for Error {
340    fn from(value: Utf8Error) -> Self {
341        Error::Utf8(value)
342    }
343}
344
345impl core::error::Error for OwnedError {
346    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
347        Some(&self.0)
348    }
349}
350
351impl Display for OwnedError {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        self.0.fmt(f)
354    }
355}
356
357impl core::error::Error for Error {
358    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
359        match self {
360            Error::Io(e) => Some(e),
361            Error::Utf8(e) => Some(e),
362        }
363    }
364}
365
366impl Display for Error {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        match self {
369            Error::Io(e) => e.fmt(f),
370            Error::Utf8(e) => e.fmt(f),
371        }
372    }
373}
374
375/// A minimal in-crate implementation of [`zeroize::Zeroize`][0].
376///
377/// This provides compile-fenced memory zeroing for [`String`]s and [`Vec`]s without needing to
378/// depend on the `zeroize` crate.
379///
380/// If the optional `zeroize` feature is enabled, then this module is replaced with `zeroize`
381/// itself.
382///
383/// [0]: https://docs.rs/zeroize/latest/zeroize/trait.Zeroize.html
384#[cfg(any(docsrs, not(feature = "zeroize")))]
385pub mod zeroize {
386    use std::{arch::asm, mem::MaybeUninit};
387
388    /// Trait for securely erasing values from memory.
389    pub trait Zeroize {
390        fn zeroize(&mut self);
391    }
392
393    impl Zeroize for Vec<u8> {
394        fn zeroize(&mut self) {
395            self.clear();
396            self.spare_capacity_mut().fill(MaybeUninit::zeroed());
397            compile_fence(self);
398        }
399    }
400
401    impl Zeroize for String {
402        fn zeroize(&mut self) {
403            unsafe { self.as_mut_vec() }.zeroize();
404        }
405    }
406
407    impl Zeroize for [u8] {
408        fn zeroize(&mut self) {
409            self.fill(0);
410            compile_fence(self);
411        }
412    }
413
414    fn compile_fence(buf: &[u8]) {
415        unsafe {
416            asm!(
417                "/* {ptr} */",
418                ptr = in(reg) buf.as_ptr(),
419                options(nostack, preserves_flags, readonly)
420            );
421        }
422    }
423}
424
425mod ffi {
426    use std::ffi::{c_char, c_int};
427
428    unsafe extern "C" {
429        pub(crate) unsafe fn readpassphrase(
430            prompt: *const c_char,
431            buf: *mut c_char,
432            bufsiz: usize,
433            flags: c_int,
434        ) -> *mut c_char;
435    }
436}