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