readpassphrase_3/lib.rs
1// Copyright 2025
2// Steven Dee
3//
4// Permission to use, copy, modify, and/or distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
10// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,
11// OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
12// DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
13// ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
15//! Lightweight, easy-to-use wrapper around the C [`readpassphrase(3)`][0] function.
16//!
17//! From the man page:
18//! > The `readpassphrase()` function displays a prompt to, and reads in a passphrase from,
19//! > `/dev/tty`. If this file is inaccessible and the [`RPP_REQUIRE_TTY`](Flags::REQUIRE_TTY) flag
20//! > is not set, `readpassphrase()` displays the prompt on the standard error output and reads
21//! > from the standard input.
22//!
23//! # Usage
24//! For the simplest of cases, where you would just like to read a password from the console into a
25//! [`String`] to use elsewhere, you can use [`getpass`]:
26//! ```no_run
27//! use readpassphrase_3::getpass;
28//! let _ = getpass(c"Enter your password: ").expect("failed reading password");
29//! ```
30//!
31//! If you need to pass [`Flags`] or to control the buffer size, then you can use
32//! [`readpassphrase`] or [`readpassphrase_into`] depending on your ownership requirements:
33//! ```no_run
34//! let mut buf = vec![0u8; 256];
35//! use readpassphrase_3::{Flags, readpassphrase};
36//! let pass: &str = readpassphrase(c"Password: ", &mut buf, Flags::default()).unwrap();
37//!
38//! use readpassphrase_3::readpassphrase_into;
39//! let pass: String = readpassphrase_into(c"Pass: ", buf, Flags::FORCELOWER).unwrap();
40//! # _ = pass;
41//! ```
42//!
43//! # Security
44//! The [`readpassphrase(3)` man page][0] says:
45//! > The calling process should zero the passphrase as soon as possible to avoid leaving the
46//! > cleartext passphrase visible in the process's address space.
47//!
48//! It is your job to ensure that this is done with the data you own, i.e.
49//! any [`Vec`] passed to [`readpassphrase`] or any [`String`] received from [`getpass`] or
50//! [`readpassphrase_into`].
51//!
52//! This crate ships with a minimal [`Zeroize`] trait that may be used for this purpose:
53//! ```no_run
54//! # use readpassphrase_3::{Flags, getpass, readpassphrase, readpassphrase_into};
55//! use readpassphrase_3::Zeroize;
56//! let mut pass = getpass(c"password: ").unwrap();
57//! // do_something_with(&pass);
58//! pass.zeroize();
59//!
60//! let mut buf = vec![0u8; 256];
61//! let res = readpassphrase(c"password: ", &mut buf, Flags::empty());
62//! // match_something_on(res);
63//! buf.zeroize();
64//!
65//! let mut pass = readpassphrase_into(c"password: ", buf, Flags::empty()).unwrap();
66//! // do_something_with(&pass);
67//! pass.zeroize();
68//! ```
69//!
70//! ## Zeroizing memory
71//! This crate works well with the [`zeroize`] crate. For example, [`zeroize::Zeroizing`] may be
72//! used to zero buffer contents regardless of a function’s control flow:
73//! ```no_run
74//! # use readpassphrase_3::{Error, Flags, PASSWORD_LEN, getpass, readpassphrase};
75//! use zeroize::Zeroizing;
76//! # fn main() -> Result<(), Error> {
77//! let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
78//! let pass = readpassphrase(c"pass: ", &mut buf, Flags::REQUIRE_TTY)?;
79//! // do_something_that_can_fail_with(pass)?;
80//!
81//! // Or alternatively:
82//! let pass = Zeroizing::new(getpass(c"pass: ")?);
83//! // do_something_that_can_fail_with(&pass)?;
84//! # Ok(())
85//! # }
86//! ```
87//!
88//! If this crate’s `zeroize` feature is enabled, then its [`Zeroize`] will be replaced by a
89//! re-export of the upstream [`zeroize::Zeroize`].
90//!
91//! # “Mismatched types” errors
92//! The prompt strings in this API are <code>&[CStr]</code>, not <code>&[str]</code>.
93//! This is because the underlying C function assumes that the prompt is a null-terminated string;
94//! were we to take `&str` instead of `&CStr`, we would need to make a copy of the prompt on every
95//! call.
96//!
97//! Most of the time, your prompts will be string literals; you can ask Rust to give you a `&CStr`
98//! literal by simply prepending `c` to the string:
99//! ```no_run
100//! # use readpassphrase_3::{Error, getpass};
101//! # fn main() -> Result<(), Error> {
102//! let _ = getpass(c"pass: ")?;
103//! // ^
104//! // |
105//! // like this
106//! # Ok(())
107//! # }
108//! ```
109//!
110//! If you need a dynamic prompt, look at [`CString`](std::ffi::CString).
111//!
112//! # Windows Limitations
113//! The Windows implementation of `readpassphrase(3)` that we are using does not yet support UTF-8
114//! in prompts; they must be ASCII. It also does not yet support flags, and always behaves as
115//! though called with [`Flags::empty()`].
116//!
117//! [0]: https://man.openbsd.org/readpassphrase
118//! [str]: prim@str "str"
119
120use std::{error, ffi::CStr, fmt, io, mem, str};
121
122use bitflags::bitflags;
123#[cfg(any(docsrs, not(feature = "zeroize")))]
124pub use our_zeroize::Zeroize;
125#[cfg(all(not(docsrs), feature = "zeroize"))]
126pub use zeroize::Zeroize;
127
128/// Size of buffer used in [`getpass`].
129///
130/// Because `readpassphrase(3)` null-terminates its string, the actual maximum password length for
131/// [`getpass`] is 255.
132pub const PASSWORD_LEN: usize = 256;
133
134bitflags! {
135 /// Flags for controlling readpassphrase.
136 ///
137 /// The default flag `ECHO_OFF` is not represented here because `bitflags` [recommends against
138 /// zero-bit flags][0]; it may be specified as either [`Flags::empty()`] or
139 /// [`Flags::default()`].
140 ///
141 /// Note that the Windows `readpassphrase(3)` implementation always acts like it has been
142 /// passed `ECHO_OFF`, i.e., the flags are ignored.
143 ///
144 /// [0]: https://docs.rs/bitflags/latest/bitflags/#zero-bit-flags
145 #[derive(Default)]
146 pub struct Flags: i32 {
147 /// Leave echo on.
148 const ECHO_ON = 0x01;
149 /// Fail if there is no tty.
150 const REQUIRE_TTY = 0x02;
151 /// Force input to lower case.
152 const FORCELOWER = 0x04;
153 /// Force input to upper case.
154 const FORCEUPPER = 0x08;
155 /// Strip the high bit from input.
156 const SEVENBIT = 0x10;
157 /// Read from stdin, not `/dev/tty`.
158 const STDIN = 0x20;
159 }
160}
161
162/// Errors that can occur in readpassphrase.
163#[derive(Debug)]
164pub enum Error {
165 /// `readpassphrase(3)` itself encountered an error.
166 Io(io::Error),
167 /// The entered password was not UTF-8.
168 Utf8(str::Utf8Error),
169}
170
171/// Reads a passphrase using `readpassphrase(3)`.
172///
173/// This function returns a <code>&[str]</code> backed by `buf`, representing a password of up to
174/// `buf.len() - 1` bytes. Any additional characters and the terminating newline are discarded.
175///
176/// # Errors
177/// Returns [`Err`] if `readpassphrase(3)` itself failed or if the entered password is not UTF-8.
178/// The former will be represented by [`Error::Io`] and the latter by [`Error::Utf8`].
179///
180/// # Security
181/// The passed buffer might contain sensitive data, even if this function returns an error.
182/// Therefore it should be zeroed as soon as possible. This can be achieved, for example, with
183/// [`zeroize::Zeroizing`]:
184/// ```no_run
185/// # use readpassphrase_3::{PASSWORD_LEN, Error, Flags, readpassphrase};
186/// use zeroize::Zeroizing;
187/// # fn main() -> Result<(), Error> {
188/// let mut buf = Zeroizing::new(vec![0u8; PASSWORD_LEN]);
189/// let pass = readpassphrase(c"Pass: ", &mut buf, Flags::default())?;
190/// # Ok(())
191/// # }
192/// ```
193/// [str]: prim@str "str"
194pub fn readpassphrase<'a>(
195 prompt: &CStr,
196 buf: &'a mut [u8],
197 flags: Flags,
198) -> Result<&'a str, Error> {
199 let prompt = prompt.as_ptr();
200 let buf_ptr = buf.as_mut_ptr().cast();
201 let bufsiz = buf.len();
202 let flags = flags.bits();
203 // SAFETY: `prompt` is a nul-terminated byte sequence, and `buf_ptr` is an allocation of at
204 // least `bufsiz` bytes, as guaranteed by `&CStr` and `&mut [u8]` respectively.
205 let res = unsafe { ffi::readpassphrase(prompt, buf_ptr, bufsiz, flags) };
206 if res.is_null() {
207 return Err(io::Error::last_os_error().into());
208 }
209 Ok(CStr::from_bytes_until_nul(buf).unwrap().to_str()?)
210}
211
212/// Reads a passphrase using `readpassphrase(3)`, returning a [`String`].
213///
214/// Internally, this function uses a buffer of [`PASSWORD_LEN`] bytes, allowing for passwords up to
215/// `PASSWORD_LEN - 1` characters (accounting for the C null terminator.) If the entered passphrase
216/// is longer, it will be truncated to the maximum length.
217///
218/// # Errors
219/// Returns [`Err`] if `readpassphrase(3)` itself failed or if the entered password is not UTF-8.
220/// The former will be represented by [`Error::Io`] and the latter by [`Error::Utf8`].
221///
222/// # Security
223/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
224/// to clear it when you are done with it:
225/// ```no_run
226/// # use readpassphrase_3::{Error, Zeroize, getpass};
227/// # fn main() -> Result<(), Error> {
228/// let mut pass = getpass(c"Pass: ")?;
229/// _ = pass;
230/// pass.zeroize();
231/// # Ok(())
232/// # }
233/// ```
234pub fn getpass(prompt: &CStr) -> Result<String, Error> {
235 let buf = Vec::with_capacity(PASSWORD_LEN);
236 Ok(readpassphrase_into(prompt, buf, Flags::empty())?)
237}
238
239/// An [`Error`] from [`readpassphrase_into`] containing the passed buffer.
240///
241/// The buffer is accessible via [`IntoError::into_bytes`][0], and the `Error` via
242/// [`IntoError::error`].
243///
244/// If [`into_bytes`][0] is not called, the buffer is automatically zeroed on drop.
245///
246/// This struct is also exported as [`OwnedError`]. That name is deprecated; please transition to
247/// using `IntoError` instead.
248///
249/// [0]: IntoError::into_bytes
250#[derive(Debug)]
251pub struct IntoError(Error, Option<Vec<u8>>);
252
253/// Reads a passphrase using `readpassphrase(3)`, returning `buf` as a [`String`].
254///
255/// This function reads a passphrase of up to `buf.capacity() - 1` bytes. If the entered passphrase
256/// is longer, it will be truncated.
257///
258/// The returned [`String`] reuses `buf`’s memory; no copies are made.
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/// # Errors
265/// Returns [`Err`] if `readpassphrase(3)` itself failed or if the entered password is not UTF-8.
266/// The former will be represented by [`Error::Io`] and the latter by [`Error::Utf8`]. The vector
267/// you moved in is also included.
268///
269/// See the docs for [`IntoError`] for more details on what you can do with this error.
270///
271/// # Security
272/// The returned `String` is owned by the caller, and it is the caller’s responsibility to clear
273/// it. This can be done via [`Zeroize`], e.g.:
274/// ```no_run
275/// # use readpassphrase_3::{
276/// # PASSWORD_LEN,
277/// # Error,
278/// # Flags,
279/// # readpassphrase_into,
280/// # };
281/// # use readpassphrase_3::Zeroize;
282/// # fn main() -> Result<(), Error> {
283/// let buf = vec![0u8; PASSWORD_LEN];
284/// let mut pass = readpassphrase_into(c"Pass: ", buf, Flags::default())?;
285/// _ = pass;
286/// pass.zeroize();
287/// # Ok(())
288/// # }
289/// ```
290pub fn readpassphrase_into(
291 prompt: &CStr,
292 mut buf: Vec<u8>,
293 flags: Flags,
294) -> Result<String, IntoError> {
295 let prompt = prompt.as_ptr();
296 let buf_ptr = buf.as_mut_ptr().cast();
297 let bufsiz = buf.capacity();
298 let flags = flags.bits();
299 // SAFETY: `prompt` from `&CStr` as above. `buf_ptr` points to an allocation of `bufsiz` bytes.
300 let res = unsafe { ffi::readpassphrase(prompt, buf_ptr, bufsiz, flags) };
301 if res.is_null() {
302 buf.clear();
303 return Err(IntoError(io::Error::last_os_error().into(), Some(buf)));
304 }
305 let nul_pos = (0..bufsiz as isize)
306 // SAFETY: `i` is within `bufsiz`, which is the size of `buf`’s allocation;
307 // `ffi::readpassphrase` initialized `buf` up through a zero byte. We scan `buf` in order;
308 // the zero byte we find is at or before the end of the initialized portion.
309 .position(|i| unsafe { *buf_ptr.offset(i) == 0 })
310 .unwrap();
311 // SAFETY: `buf` is initialized at least up to `nul_pos`.
312 unsafe { buf.set_len(nul_pos) };
313 String::from_utf8(buf).map_err(|err| {
314 let res = err.utf8_error();
315 IntoError(res.into(), Some(err.into_bytes()))
316 })
317}
318
319#[deprecated(since = "0.10.0", note = "please use `IntoError`")]
320pub use IntoError as OwnedError;
321
322/// Deprecated alias for [`readpassphrase_into`].
323#[deprecated(since = "0.10.0", note = "please use `readpassphrase_into`")]
324pub fn readpassphrase_owned(
325 prompt: &CStr,
326 buf: Vec<u8>,
327 flags: Flags,
328) -> Result<String, IntoError> {
329 readpassphrase_into(prompt, buf, flags)
330}
331
332impl IntoError {
333 /// Return the [`Error`] corresponding to this.
334 pub fn error(&self) -> &Error {
335 &self.0
336 }
337
338 /// Returns the buffer that was passed to [`readpassphrase_into`].
339 ///
340 /// # Panics
341 /// Panics if [`IntoError::take`] was called before this.
342 pub fn into_bytes(mut self) -> Vec<u8> {
343 self.1.take().unwrap()
344 }
345
346 /// Returns the buffer that was passed to [`readpassphrase_into`].
347 ///
348 /// If called multiple times, returns [`Vec::new`].
349 #[deprecated(since = "0.10.0", note = "please use `into_bytes` instead")]
350 pub fn take(&mut self) -> Vec<u8> {
351 self.1.take().unwrap_or_default()
352 }
353}
354
355impl error::Error for IntoError {
356 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
357 Some(&self.0)
358 }
359}
360
361impl fmt::Display for IntoError {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 self.0.fmt(f)
364 }
365}
366
367impl Drop for IntoError {
368 fn drop(&mut self) {
369 self.1.take().as_mut().map(Zeroize::zeroize);
370 }
371}
372
373impl From<IntoError> for Error {
374 fn from(mut value: IntoError) -> Self {
375 mem::replace(&mut value.0, Error::Io(io::ErrorKind::Other.into()))
376 }
377}
378
379impl From<io::Error> for Error {
380 fn from(value: io::Error) -> Self {
381 Error::Io(value)
382 }
383}
384
385impl From<str::Utf8Error> for Error {
386 fn from(value: str::Utf8Error) -> Self {
387 Error::Utf8(value)
388 }
389}
390
391impl error::Error for Error {
392 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
393 Some(match self {
394 Error::Io(e) => e,
395 Error::Utf8(e) => e,
396 })
397 }
398}
399
400impl fmt::Display for Error {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 match self {
403 Error::Io(e) => e.fmt(f),
404 Error::Utf8(e) => e.fmt(f),
405 }
406 }
407}
408
409#[cfg(any(docsrs, not(feature = "zeroize")))]
410mod our_zeroize {
411 use std::{arch::asm, mem::MaybeUninit};
412
413 /// A minimal in-crate implementation of a subset of [`zeroize::Zeroize`].
414 ///
415 /// This provides compile-fenced memory zeroing for [`String`]s and [`Vec`]s without needing to
416 /// depend on the `zeroize` crate.
417 ///
418 /// If the optional `zeroize` feature is enabled, then the trait is replaced with a re-export of
419 /// `zeroize::Zeroize` itself.
420 pub trait Zeroize {
421 fn zeroize(&mut self);
422 }
423
424 impl Zeroize for Vec<u8> {
425 fn zeroize(&mut self) {
426 self.clear();
427 let buf = self.spare_capacity_mut();
428 buf.fill(MaybeUninit::zeroed());
429 compile_fence(buf);
430 }
431 }
432
433 impl Zeroize for String {
434 fn zeroize(&mut self) {
435 // SAFETY: we clear the string.
436 unsafe { self.as_mut_vec() }.zeroize();
437 }
438 }
439
440 impl Zeroize for [u8] {
441 fn zeroize(&mut self) {
442 self.fill(0);
443 compile_fence(self);
444 }
445 }
446
447 fn compile_fence<T>(buf: &[T]) {
448 unsafe {
449 asm!(
450 "/* {ptr} */",
451 ptr = in(reg) buf.as_ptr(),
452 options(nostack, preserves_flags, readonly)
453 );
454 }
455 }
456}
457
458mod ffi {
459 use std::ffi::{c_char, c_int};
460
461 extern "C" {
462 pub(crate) fn readpassphrase(
463 prompt: *const c_char,
464 buf: *mut c_char,
465 bufsiz: usize,
466 flags: c_int,
467 ) -> *mut c_char;
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn test_empty() {
477 let err = readpassphrase_into(c"pass", Vec::new(), Flags::empty()).unwrap_err();
478 let Error::Io(err) = err.into() else {
479 panic!();
480 };
481 #[cfg(not(windows))]
482 assert_eq!(io::ErrorKind::InvalidInput, err.kind());
483 #[cfg(windows)]
484 {
485 _ = err
486 };
487
488 let mut buf = Vec::new();
489 let err = readpassphrase(c"pass", &mut buf, Flags::empty()).unwrap_err();
490 let Error::Io(err) = err else {
491 panic!();
492 };
493 #[cfg(not(windows))]
494 assert_eq!(io::ErrorKind::InvalidInput, err.kind());
495 #[cfg(windows)]
496 {
497 _ = err
498 };
499 }
500
501 #[test]
502 fn test_zeroize() {
503 let mut buf = "test".to_string();
504 buf.zeroize();
505 unsafe { buf.as_mut_vec().set_len(4) };
506 assert_eq!("\0\0\0\0", &buf);
507 let mut buf = vec![1u8; 15];
508 unsafe { buf.set_len(0) };
509 let x = buf.spare_capacity_mut()[0];
510 assert_eq!(unsafe { x.assume_init() }, 1);
511 buf.zeroize();
512 unsafe { buf.set_len(15) };
513 assert_eq!(vec![0u8; 15], buf);
514 let mut buf = vec![1u8; 2];
515 unsafe { buf.set_len(1) };
516 let slice = &mut *buf;
517 slice.zeroize();
518 unsafe { buf.set_len(2) };
519 assert_eq!(vec![0u8, 1], buf);
520 }
521}