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::{Error, RppFlags, readpassphrase};
86/// # use zeroize::Zeroizing;
87/// # fn main() -> Result<(), Error> {
88/// let pass = Zeroizing::new(readpassphrase(c"Pass: ", RppFlags::default())?);
89/// # Ok(())
90/// # }
91/// ```
92pub fn readpassphrase_buf(
93 prompt: &CStr,
94 #[allow(unused_mut)] mut buf: Vec<u8>,
95 flags: RppFlags,
96) -> Result<String, Error> {
97 #[cfg(feature = "zeroize")]
98 let mut buf = Zeroizing::new(buf);
99 unsafe {
100 let res = ffi::readpassphrase(
101 prompt.as_ptr(),
102 buf.as_mut_ptr().cast(),
103 buf.len(),
104 flags.bits(),
105 );
106 if res.is_null() {
107 return Err(io::Error::last_os_error().into());
108 }
109 }
110 let nul_pos = buf
111 .iter()
112 .position(|&b| b == 0)
113 .ok_or(io::Error::from(io::ErrorKind::InvalidData))?;
114 buf.truncate(nul_pos);
115 let _ = str::from_utf8(&buf)?;
116 Ok(unsafe { String::from_utf8_unchecked(mem::take(&mut buf)) })
117}
118
119/// Reads a passphrase using `readpassphrase(3)` info the passed buffer.
120/// Returns a string slice from that buffer. Does not zero memory; this
121/// should be done out of band, for example by using `Zeroizing<Vec<u8>>`.
122/// For example:
123///
124/// ```no_run
125/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase_inplace};
126/// # use zeroize::Zeroizing;
127/// # fn main() -> Result<(), Error> {
128/// let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
129/// let pass = readpassphrase_inplace(c"Pass: ", &mut buf, RppFlags::default())?;
130/// # Ok(())
131/// # }
132/// ```
133pub fn readpassphrase_inplace<'a>(
134 prompt: &CStr,
135 buf: &'a mut [u8],
136 flags: RppFlags,
137) -> Result<&'a str, Error> {
138 unsafe {
139 let res = ffi::readpassphrase(
140 prompt.as_ptr(),
141 buf.as_mut_ptr().cast(),
142 buf.len(),
143 flags.bits(),
144 );
145 if res.is_null() {
146 return Err(io::Error::last_os_error().into());
147 }
148 }
149 let res = CStr::from_bytes_until_nul(buf)?;
150 Ok(res.to_str()?)
151}
152
153impl From<io::Error> for Error {
154 fn from(value: io::Error) -> Self {
155 Error::Io(value)
156 }
157}
158
159impl From<Utf8Error> for Error {
160 fn from(value: Utf8Error) -> Self {
161 Error::Utf8(value)
162 }
163}
164
165impl From<FromBytesUntilNulError> for Error {
166 fn from(value: FromBytesUntilNulError) -> Self {
167 Error::CStr(value)
168 }
169}
170
171impl Display for Error {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 match self {
174 Error::Io(e) => e.fmt(f),
175 Error::Utf8(e) => e.fmt(f),
176 Error::CStr(e) => e.fmt(f),
177 }
178 }
179}
180
181mod ffi {
182 use std::ffi::{c_char, c_int};
183
184 unsafe extern "C" {
185 pub(crate) unsafe fn readpassphrase(
186 prompt: *const c_char,
187 buf: *mut c_char,
188 bufsiz: usize,
189 flags: c_int,
190 ) -> *mut c_char;
191 }
192}