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}, fmt::Display, io, mem, str::Utf8Error
11};
12
13use bitflags::bitflags;
14#[cfg(feature = "zeroize")]
15use zeroize::Zeroizing;
16
17const PASSWORD_LEN: usize = 256;
18
19bitflags! {
20    /// Flags for controlling readpassphrase
21    pub struct RppFlags: i32 {
22        /// Furn off echo (default)
23        const ECHO_OFF    = 0x00;
24        /// Leave echo on
25        const ECHO_ON     = 0x01;
26        /// Fail if there is no tty
27        const REQUIRE_TTY = 0x02;
28        /// Force input to lower case
29        const FORCELOWER  = 0x04;
30        /// Force input to upper case
31        const FORCEUPPER  = 0x08;
32        /// Strip the high bit from input
33        const SEVENBIT    = 0x10;
34        /// Read from stdin, not /dev/tty
35        const STDIN       = 0x20;
36    }
37}
38
39impl Default for RppFlags {
40    fn default() -> Self {
41        Self::ECHO_OFF
42    }
43}
44
45#[derive(Debug)]
46pub enum Error {
47    Io(io::Error),
48    Utf8(Utf8Error),
49    CStr(FromBytesUntilNulError),
50}
51
52/// Reads a passphrase using `readpassphrase(3)`, returning it as a `String`.
53/// Internally uses a buffer of `PASSWORD_LEN` bytes, allowing for passwords
54/// up to `PASSWORD_LEN - 1` characters (including the null terminator.)
55pub fn readpassphrase(prompt: &CStr, flags: RppFlags) -> Result<String, Error> {
56    readpassphrase_buf(prompt, vec![0u8; PASSWORD_LEN], flags)
57}
58
59/// Reads a passphrase using `readpassphrase(3)` into the passed buffer.
60/// Returns a `String` consisting of the same memory from the buffer, or
61/// else zeroes the buffer on error.
62pub fn readpassphrase_buf(
63    prompt: &CStr,
64    #[allow(unused_mut)] mut buf: Vec<u8>,
65    flags: RppFlags,
66) -> Result<String, Error> {
67    #[cfg(feature = "zeroize")]
68    let mut buf = Zeroizing::new(buf);
69    unsafe {
70        let res = ffi::readpassphrase(
71            prompt.as_ptr(),
72            buf.as_mut_ptr().cast(),
73            buf.len(),
74            flags.bits(),
75        );
76        if res.is_null() {
77            return Err(io::Error::last_os_error().into());
78        }
79    }
80    let nul_pos = buf
81        .iter()
82        .position(|&b| b == 0)
83        .ok_or(io::Error::from(io::ErrorKind::InvalidData))?;
84    buf.truncate(nul_pos);
85    let _ = str::from_utf8(&buf)?;
86    Ok(unsafe { String::from_utf8_unchecked(mem::take(&mut buf)) })
87}
88
89/// Reads a passphrase using `readpassphrase(3)` info the passed buffer.
90/// Returns a string slice from that buffer. Does not zero memory; this
91/// should be done out of band, for example by using `Zeroizing<Vec<u8>>`.
92pub fn readpassphrase_inplace<'a>(
93    prompt: &CStr,
94    buf: &'a mut [u8],
95    flags: RppFlags,
96) -> Result<&'a str, Error> {
97    unsafe {
98        let res = ffi::readpassphrase(
99            prompt.as_ptr(),
100            buf.as_mut_ptr().cast(),
101            buf.len(),
102            flags.bits(),
103        );
104        if res.is_null() {
105            return Err(io::Error::last_os_error().into());
106        }
107    }
108    let res = CStr::from_bytes_until_nul(buf)?;
109    Ok(res.to_str()?)
110}
111
112impl From<io::Error> for Error {
113    fn from(value: io::Error) -> Self {
114        Error::Io(value)
115    }
116}
117
118impl From<Utf8Error> for Error {
119    fn from(value: Utf8Error) -> Self {
120        Error::Utf8(value)
121    }
122}
123
124impl From<FromBytesUntilNulError> for Error {
125    fn from(value: FromBytesUntilNulError) -> Self {
126        Error::CStr(value)
127    }
128}
129
130impl Display for Error {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Error::Io(e) => e.fmt(f),
134            Error::Utf8(e) => e.fmt(f),
135            Error::CStr(e) => e.fmt(f),
136        }
137    }
138}
139
140mod ffi {
141    use std::ffi::{c_char, c_int};
142
143    unsafe extern "C" {
144        pub(crate) unsafe fn readpassphrase(
145            prompt: *const c_char,
146            buf: *mut c_char,
147            bufsiz: usize,
148            flags: c_int,
149        ) -> *mut c_char;
150    }
151}