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 the
4// LICENSE file in the project root for details.
5//
6// The readpassphrase source and header are copyright 2000-2002, 2007, 2010 Todd
7// C. Miller.
8
9use std::{
10    ffi::{CStr, FromBytesUntilNulError},
11    io, mem,
12    str::Utf8Error,
13};
14
15use bitflags::bitflags;
16use thiserror::Error;
17#[cfg(feature = "zeroize")]
18use zeroize::Zeroizing;
19
20const 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(Error, Debug)]
49pub enum Error {
50    #[error(transparent)]
51    IoError(#[from] io::Error),
52    #[error(transparent)]
53    Utf8Error(#[from] Utf8Error),
54    #[error(transparent)]
55    CStrError(#[from] FromBytesUntilNulError),
56}
57
58/// Reads a passphrase using `readpassphrase(3)`, returning it as a `String`.
59/// Internally uses a buffer of `PASSWORD_LEN` bytes, allowing for passwords
60/// up to `PASSWORD_LEN - 1` characters (including the null terminator.)
61pub fn readpassphrase(prompt: &CStr, flags: RppFlags) -> Result<String, Error> {
62    readpassphrase_buf(prompt, vec![0u8; PASSWORD_LEN], flags)
63}
64
65/// Reads a passphrase using `readpassphrase(3)` into the passed buffer.
66/// Returns a `String` consisting of the same memory from the buffer, or
67/// else zeroes the buffer on error.
68pub fn readpassphrase_buf(
69    prompt: &CStr,
70    #[allow(unused_mut)] mut buf: Vec<u8>,
71    flags: RppFlags,
72) -> Result<String, Error> {
73    #[cfg(feature = "zeroize")]
74    let mut buf = Zeroizing::new(buf);
75    unsafe {
76        let res = ffi::readpassphrase(
77            prompt.as_ptr(),
78            buf.as_mut_ptr().cast(),
79            buf.len(),
80            flags.bits(),
81        );
82        if res.is_null() {
83            return Err(io::Error::last_os_error().into());
84        }
85    }
86    let nul_pos = buf
87        .iter()
88        .position(|&b| b == 0)
89        .ok_or(io::Error::from(io::ErrorKind::InvalidData))?;
90    buf.truncate(nul_pos);
91    let _ = str::from_utf8(&buf)?;
92    Ok(unsafe { String::from_utf8_unchecked(mem::take(&mut buf)) })
93}
94
95/// Reads a passphrase using `readpassphrase(3)` info the passed buffer.
96/// Returns a string slice from that buffer. Does not zero memory; this
97/// should be done out of band, for example by using `Zeroizing<Vec<u8>>`.
98pub fn readpassphrase_inplace<'a>(
99    prompt: &CStr,
100    buf: &'a mut [u8],
101    flags: RppFlags,
102) -> Result<&'a str, Error> {
103    unsafe {
104        let res = ffi::readpassphrase(
105            prompt.as_ptr(),
106            buf.as_mut_ptr().cast(),
107            buf.len(),
108            flags.bits(),
109        );
110        if res.is_null() {
111            return Err(io::Error::last_os_error().into());
112        }
113    }
114    let res = CStr::from_bytes_until_nul(buf)?;
115    Ok(res.to_str()?)
116}
117
118mod ffi {
119    use std::ffi::{c_char, c_int};
120
121    unsafe extern "C" {
122        pub(crate) unsafe fn readpassphrase(
123            prompt: *const c_char,
124            buf: *mut c_char,
125            bufsiz: usize,
126            flags: c_int,
127        ) -> *mut c_char;
128    }
129}