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
9//! This library endeavors to expose a thin wrapper around OpenBSD’s [`readpassphrase(3)`][0]
10//! function.
11//!
12//! Three different interfaces are exposed; for most purposes, you will want to use either
13//! [`getpass`] (for simple password entry) or [`readpassphrase`] (when you need flags from
14//! `readpassphrase(3)` or need more control over the memory.)
15//!
16//! The [`readpassphrase_owned`] function is a bit more niche; it may be used when you need a
17//! [`String`] output but need to pass flags or control the buffer size (vs [`getpass`].)
18//!
19//! [0]: https://man.openbsd.org/readpassphrase
20
21use std::{
22 ffi::{CStr, FromBytesUntilNulError},
23 fmt::Display,
24 io,
25 str::Utf8Error,
26};
27
28use bitflags::bitflags;
29#[cfg(feature = "zeroize")]
30use zeroize::Zeroize;
31
32pub const PASSWORD_LEN: usize = 256;
33
34bitflags! {
35 /// Flags for controlling readpassphrase
36 pub struct RppFlags: i32 {
37 /// Furn off echo (default)
38 const ECHO_OFF = 0x00;
39 /// Leave echo on
40 const ECHO_ON = 0x01;
41 /// Fail if there is no tty
42 const REQUIRE_TTY = 0x02;
43 /// Force input to lower case
44 const FORCELOWER = 0x04;
45 /// Force input to upper case
46 const FORCEUPPER = 0x08;
47 /// Strip the high bit from input
48 const SEVENBIT = 0x10;
49 /// Read from stdin, not /dev/tty
50 const STDIN = 0x20;
51 }
52}
53
54impl Default for RppFlags {
55 fn default() -> Self {
56 Self::ECHO_OFF
57 }
58}
59
60#[derive(Debug)]
61pub enum Error {
62 Io(io::Error),
63 Utf8(Utf8Error),
64 CStr(FromBytesUntilNulError),
65}
66
67/// Reads a passphrase using `readpassphrase(3)`.
68///
69/// # Security
70/// The passed buffer might contain sensitive data, even if this function returns an error (for
71/// example, if the contents are not valid UTF-8.) It is often considered good practice to zero
72/// this memory after you’re done with it, for example by using [`zeroize`]:
73/// ```no_run
74/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, 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, RppFlags::default())?;
79/// # Ok(())
80/// # }
81/// ```
82pub fn readpassphrase<'a>(
83 prompt: &CStr,
84 buf: &'a mut [u8],
85 flags: RppFlags,
86) -> Result<&'a str, Error> {
87 unsafe {
88 let res = ffi::readpassphrase(
89 prompt.as_ptr(),
90 buf.as_mut_ptr().cast(),
91 buf.len(),
92 flags.bits(),
93 );
94 if res.is_null() {
95 return Err(io::Error::last_os_error().into());
96 }
97 }
98 Ok(CStr::from_bytes_until_nul(buf)?.to_str()?)
99}
100
101/// Reads a passphrase using `readpassphrase(3)`, returning it as a [`String`].
102///
103/// Internally, this function uses a buffer of [`PASSWORD_LEN`] bytes, allowing for passwords up to
104/// `PASSWORD_LEN - 1` characters (accounting for the C null terminator.) The passed flags are
105/// always the defaults, i.e., [`RppFlags::ECHO_OFF`].
106///
107/// # Security
108/// If the [`zeroize`] feature of this crate is disabled, then this function can leak sensitive
109/// data on failure, e.g. if the entered passphrase is not valid UTF-8. There is no way around this
110/// (other than using the default `zeroize` feature), so if you must turn that feature off and are
111/// concerned about this, then you should use the [`readpassphrase`] function instead.
112///
113/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
114/// to clear it when you are done with it, for example by using [`zeroize`]:
115/// ```no_run
116/// # use readpassphrase_3::{Error, getpass};
117/// use zeroize::Zeroizing;
118/// # fn main() -> Result<(), Error> {
119/// let pass = Zeroizing::new(getpass(c"Pass: ")?);
120/// # Ok(())
121/// # }
122/// ```
123pub fn getpass(prompt: &CStr) -> Result<String, Error> {
124 #[allow(unused_mut, unused_variables)]
125 readpassphrase_owned(prompt, vec![0u8; PASSWORD_LEN], RppFlags::empty()).map_err(
126 |(e, mut buf)| {
127 #[cfg(feature = "zeroize")]
128 buf.zeroize();
129 e
130 },
131 )
132}
133
134/// Reads a passphrase using `readpassphrase(3)` using the passed buffer’s memory.
135///
136/// The returned [`String`] uses `buf`’s memory; on failure, this memory is returned to the caller in
137/// the second argument of the `Err` tuple.
138///
139/// # Security
140/// The returned `String` is owned by the caller, and therefore it is the caller’s responsibility
141/// to clear it when you are done with it. You may also wish to zero the returned buffer on error,
142/// as it may still contain sensitive data, for example if the password was not valid UTF-8.
143///
144/// This can be done via [`zeroize`], e.g.:
145/// ```no_run
146/// # use readpassphrase_3::{PASSWORD_LEN, Error, RppFlags, readpassphrase_owned};
147/// use zeroize::{Zeroizing, Zeroize};
148/// # fn main() -> Result<(), Error> {
149/// let buf = vec![0u8; PASSWORD_LEN];
150/// let pass = Zeroizing::new(
151/// readpassphrase_owned(c"Pass: ", buf, RppFlags::default())
152/// .map_err(|(e, mut buf)| { buf.zeroize(); e })?
153/// );
154/// # Ok(())
155/// # }
156/// ```
157pub fn readpassphrase_owned(
158 prompt: &CStr,
159 #[allow(unused_mut)] mut buf: Vec<u8>,
160 flags: RppFlags,
161) -> Result<String, (Error, Vec<u8>)> {
162 match readpassphrase(prompt, &mut buf, flags) {
163 Ok(res) => {
164 let len = res.len();
165 buf.truncate(len);
166 Ok(unsafe { String::from_utf8_unchecked(buf) })
167 }
168
169 Err(e) => Err((e, buf)),
170 }
171}
172
173impl From<io::Error> for Error {
174 fn from(value: io::Error) -> Self {
175 Error::Io(value)
176 }
177}
178
179impl From<Utf8Error> for Error {
180 fn from(value: Utf8Error) -> Self {
181 Error::Utf8(value)
182 }
183}
184
185impl From<FromBytesUntilNulError> for Error {
186 fn from(value: FromBytesUntilNulError) -> Self {
187 Error::CStr(value)
188 }
189}
190
191impl core::error::Error for Error {
192 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
193 match self {
194 Error::Io(e) => Some(e),
195 Error::Utf8(e) => Some(e),
196 Error::CStr(e) => Some(e),
197 }
198 }
199}
200
201impl Display for Error {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 match self {
204 Error::Io(e) => e.fmt(f),
205 Error::Utf8(e) => e.fmt(f),
206 Error::CStr(e) => e.fmt(f),
207 }
208 }
209}
210
211mod ffi {
212 use std::ffi::{c_char, c_int};
213
214 unsafe extern "C" {
215 pub(crate) unsafe fn readpassphrase(
216 prompt: *const c_char,
217 buf: *mut c_char,
218 bufsiz: usize,
219 flags: c_int,
220 ) -> *mut c_char;
221 }
222}