readpassphrase_3/
lib.rs

1// Copyright 2025 Steven Dee.
2//
3// This project is dual licensed under the MIT and Apache 2.0 licenses. See
4// the 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
9use std::{
10    ffi::{CStr, FromBytesUntilNulError},
11    fmt::Display,
12    io, mem,
13    str::Utf8Error,
14};
15
16use bitflags::bitflags;
17#[cfg(feature = "zeroize")]
18use zeroize::Zeroizing;
19
20pub const PASSWORD_LEN: usize = 256;
21
22bitflags! {
23    /// Flags for controlling readpassphrase
24    pub struct RppFlags: i32 {
25        /// Furn off echo (default)
26        const ECHO_OFF    = 0x00;
27        /// Leave echo on
28        const ECHO_ON     = 0x01;
29        /// Fail if there is no tty
30        const REQUIRE_TTY = 0x02;
31        /// Force input to lower case
32        const FORCELOWER  = 0x04;
33        /// Force input to upper case
34        const FORCEUPPER  = 0x08;
35        /// Strip the high bit from input
36        const SEVENBIT    = 0x10;
37        /// Read from stdin, not /dev/tty
38        const STDIN       = 0x20;
39    }
40}
41
42impl Default for RppFlags {
43    fn default() -> Self {
44        Self::ECHO_OFF
45    }
46}
47
48#[derive(Debug)]
49pub enum Error {
50    Io(io::Error),
51    Utf8(Utf8Error),
52    CStr(FromBytesUntilNulError),
53}
54
55/// Reads a passphrase using `readpassphrase(3)`, returning it as a `String`.
56/// Internally uses a buffer of `PASSWORD_LEN` bytes, allowing for passwords
57/// up to `PASSWORD_LEN - 1` characters (including the null terminator.)
58///
59/// # Security
60/// The returned `String` is not cleared on success; it is the caller’s
61/// responsibility to do so, e.g.:
62///
63/// ```no_run
64/// # use readpassphrase_3::{Error, RppFlags, readpassphrase};
65/// # use zeroize::Zeroizing;
66/// # fn main() -> Result<(), Error> {
67/// let pass = Zeroizing::new(readpassphrase(c"Pass: ", RppFlags::default())?);
68/// # Ok(())
69/// # }
70/// ```
71pub fn readpassphrase(prompt: &CStr, flags: RppFlags) -> Result<String, Error> {
72    readpassphrase_buf(prompt, vec![0u8; PASSWORD_LEN], flags)
73}
74
75/// Reads a passphrase using `readpassphrase(3)` into the passed buffer.
76/// Returns a `String` consisting of the same memory from the buffer. If
77/// the `zeroize` feature is enabled (which it is by default), memory is
78/// cleared on errors.
79///
80/// # Security
81/// The returned `String` is not cleared on success; it is the caller’s
82/// responsibility to do so, e.g.:
83///
84/// ```no_run
85/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase_buf};
86/// # use zeroize::Zeroizing;
87/// # fn main() -> Result<(), Error> {
88/// let buf = vec![0u8; PASSWORD_LEN];
89/// let pass = Zeroizing::new(readpassphrase_buf(c"Pass: ", buf, RppFlags::default())?);
90/// # Ok(())
91/// # }
92/// ```
93pub fn readpassphrase_buf(
94    prompt: &CStr,
95    #[allow(unused_mut)] mut buf: Vec<u8>,
96    flags: RppFlags,
97) -> Result<String, Error> {
98    #[cfg(feature = "zeroize")]
99    let mut buf = Zeroizing::new(buf);
100    unsafe {
101        let res = ffi::readpassphrase(
102            prompt.as_ptr(),
103            buf.as_mut_ptr().cast(),
104            buf.len(),
105            flags.bits(),
106        );
107        if res.is_null() {
108            return Err(io::Error::last_os_error().into());
109        }
110    }
111    let nul_pos = buf
112        .iter()
113        .position(|&b| b == 0)
114        .ok_or(io::Error::from(io::ErrorKind::InvalidData))?;
115    buf.truncate(nul_pos);
116    let _ = str::from_utf8(&buf)?;
117    Ok(unsafe { String::from_utf8_unchecked(mem::take(&mut buf)) })
118}
119
120/// Reads a passphrase using `readpassphrase(3)` into the passed buffer.
121/// Returns a string slice from that buffer.
122///
123/// # Security
124/// Does not zero memory; this should be done out of band, for example by
125/// using `Zeroizing<Vec<u8>>`:
126/// ```no_run
127/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase_inplace};
128/// # use zeroize::Zeroizing;
129/// # fn main() -> Result<(), Error> {
130/// let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
131/// let pass = readpassphrase_inplace(c"Pass: ", &mut buf, RppFlags::default())?;
132/// # Ok(())
133/// # }
134/// ```
135pub fn readpassphrase_inplace<'a>(
136    prompt: &CStr,
137    buf: &'a mut [u8],
138    flags: RppFlags,
139) -> Result<&'a str, Error> {
140    unsafe {
141        let res = ffi::readpassphrase(
142            prompt.as_ptr(),
143            buf.as_mut_ptr().cast(),
144            buf.len(),
145            flags.bits(),
146        );
147        if res.is_null() {
148            return Err(io::Error::last_os_error().into());
149        }
150    }
151    let res = CStr::from_bytes_until_nul(buf)?;
152    Ok(res.to_str()?)
153}
154
155impl From<io::Error> for Error {
156    fn from(value: io::Error) -> Self {
157        Error::Io(value)
158    }
159}
160
161impl From<Utf8Error> for Error {
162    fn from(value: Utf8Error) -> Self {
163        Error::Utf8(value)
164    }
165}
166
167impl From<FromBytesUntilNulError> for Error {
168    fn from(value: FromBytesUntilNulError) -> Self {
169        Error::CStr(value)
170    }
171}
172
173impl Display for Error {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        match self {
176            Error::Io(e) => e.fmt(f),
177            Error::Utf8(e) => e.fmt(f),
178            Error::CStr(e) => e.fmt(f),
179        }
180    }
181}
182
183mod ffi {
184    use std::ffi::{c_char, c_int};
185
186    unsafe extern "C" {
187        pub(crate) unsafe fn readpassphrase(
188            prompt: *const c_char,
189            buf: *mut c_char,
190            bufsiz: usize,
191            flags: c_int,
192        ) -> *mut c_char;
193    }
194}