├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── cc └── helper.c └── src ├── command.rs ├── const_time.rs ├── crypt.rs ├── drop_zeroed.rs ├── errno.rs ├── error.rs ├── input.rs ├── main.rs ├── memory_lock.rs └── password_bank.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "argv" 7 | version = "0.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e9fec65789d8c87d48232b6f988ca269e2729ee9f878ffe0631b82b5695ab8d9" 10 | 11 | [[package]] 12 | name = "cc" 13 | version = "1.0.72" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "getrandom" 25 | version = "0.1.16" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 28 | dependencies = [ 29 | "cfg-if", 30 | "libc", 31 | "wasi", 32 | ] 33 | 34 | [[package]] 35 | name = "kindly" 36 | version = "0.1.0" 37 | dependencies = [ 38 | "argv", 39 | "cc", 40 | "cfg-if", 41 | "libc", 42 | "memsec", 43 | "rpassword", 44 | "thiserror", 45 | "unixstring", 46 | ] 47 | 48 | [[package]] 49 | name = "libc" 50 | version = "0.2.107" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" 53 | 54 | [[package]] 55 | name = "memsec" 56 | version = "0.6.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "2af4f95d8737f4ffafbd1fb3c703cdc898868a244a59786793cba0520ebdcbdd" 59 | dependencies = [ 60 | "getrandom", 61 | "libc", 62 | "winapi", 63 | ] 64 | 65 | [[package]] 66 | name = "proc-macro2" 67 | version = "1.0.32" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" 70 | dependencies = [ 71 | "unicode-xid", 72 | ] 73 | 74 | [[package]] 75 | name = "quote" 76 | version = "1.0.10" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 79 | dependencies = [ 80 | "proc-macro2", 81 | ] 82 | 83 | [[package]] 84 | name = "rpassword" 85 | version = "5.0.1" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" 88 | dependencies = [ 89 | "libc", 90 | "winapi", 91 | ] 92 | 93 | [[package]] 94 | name = "syn" 95 | version = "1.0.81" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" 98 | dependencies = [ 99 | "proc-macro2", 100 | "quote", 101 | "unicode-xid", 102 | ] 103 | 104 | [[package]] 105 | name = "thiserror" 106 | version = "1.0.30" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 109 | dependencies = [ 110 | "thiserror-impl", 111 | ] 112 | 113 | [[package]] 114 | name = "thiserror-impl" 115 | version = "1.0.30" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 118 | dependencies = [ 119 | "proc-macro2", 120 | "quote", 121 | "syn", 122 | ] 123 | 124 | [[package]] 125 | name = "unicode-xid" 126 | version = "0.2.2" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 129 | 130 | [[package]] 131 | name = "unixstring" 132 | version = "0.2.6" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "d3a1bde53d3a7210b30ebd12b63731378552b1347aa461b30f0328911b041324" 135 | dependencies = [ 136 | "libc", 137 | ] 138 | 139 | [[package]] 140 | name = "wasi" 141 | version = "0.9.0+wasi-snapshot-preview1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 144 | 145 | [[package]] 146 | name = "winapi" 147 | version = "0.3.9" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 150 | dependencies = [ 151 | "winapi-i686-pc-windows-gnu", 152 | "winapi-x86_64-pc-windows-gnu", 153 | ] 154 | 155 | [[package]] 156 | name = "winapi-i686-pc-windows-gnu" 157 | version = "0.4.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 160 | 161 | [[package]] 162 | name = "winapi-x86_64-pc-windows-gnu" 163 | version = "0.4.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 166 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kindly" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Vinícius Rodrigues Miguel "] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [build-dependencies] 10 | cc = "1.0.68" 11 | libc = "0.2.107" 12 | 13 | [dependencies] 14 | argv = "0.1.4" 15 | libc = "0.2.107" 16 | memsec = "0.6.0" 17 | cfg-if = "1.0.0" 18 | thiserror = "1.0.30" 19 | rpassword = "5.0.1" 20 | unixstring = "0.2.6" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vinícius R. Miguel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kindly 2 | 3 | `kindly` is a (hopefully) well-commented Rust implementation of a set-user-ID-_root_ program, similar to `sudo` but in a much reduced way. 4 | 5 | __Notice__: this is not a security-hardened application (although it does take a few security measures, described below) and does not aim to replace battle-hardened tools like `sudo` or `doas`. 6 | 7 | * Locks all memory pages mapped into its address space in order to avoid leaking information if sent to swap 8 | * Reads the password from a tty using [`rpassword`](https://crates.io/crates/rpassword) 9 | * Zeroes and drops the unencrypted password as soon as it is no longer needed through non-elidable operations 10 | * Attempts to avoid timing attacks through ["constant-time"](https://www.chosenplaintext.ca/articles/beginners-guide-constant-time-cryptography.html) byte comparisons 11 | 12 | ## Building 13 | 14 | ```shell 15 | # Let's get the code and build it 16 | git clone https://github.com/vrmiguel/kindly 17 | cd kindly && cargo build --release 18 | 19 | # We now need to make `kindly` officially a set-user-ID-root program by enabling the set-user-ID bit for it 20 | sudo chown root:root target/release/kindly && sudo chmod u+s target/release/kindly 21 | ``` 22 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use cc; 2 | 3 | fn main() { 4 | println!("cargo:rustc-link-lib=crypt"); 5 | 6 | cc::Build::new().file("cc/helper.c").compile("helper"); 7 | 8 | println!("cargo:rerun-if-changed=cc/helper.c"); 9 | } 10 | -------------------------------------------------------------------------------- /cc/helper.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef MCL_ONFAULT 4 | const int _MCL_ONFAULT = MCL_ONFAULT; 5 | #else 6 | const int _MCL_ONFAULT = 4; 7 | #endif -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | //! What `kindly` needs to run commands 2 | 3 | use std::fmt::Display; 4 | use std::os::unix::process::ExitStatusExt; 5 | use std::process::ExitStatus; 6 | use std::{ffi::OsStr, process::Command}; 7 | 8 | use crate::error::{Error, Result}; 9 | 10 | /// Represents the termination status of a command 11 | pub enum TerminationStatus { 12 | /// If the given command was interrupted through a signal, this variant holds the value of that signal 13 | Signaled(i32), 14 | /// If the command terminated normally (i.e. not signal interrupted), this variant holds the exit code of the command 15 | TerminatedNormally(i32), 16 | } 17 | 18 | impl TerminationStatus { 19 | /// Returns true if the command terminated normally and with exit code 0 20 | pub fn is_ok(&self) -> bool { 21 | matches!(self, TerminationStatus::TerminatedNormally(0)) 22 | } 23 | 24 | /// Returns either the code of this command (if terminated normally) or the value of the signal that killed it (if signal interrupted) 25 | pub fn code_or_signal(&self) -> i32 { 26 | match self { 27 | TerminationStatus::Signaled(signal) => *signal, 28 | TerminationStatus::TerminatedNormally(code) => *code, 29 | } 30 | } 31 | } 32 | 33 | impl Display for TerminationStatus { 34 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 35 | match self { 36 | TerminationStatus::Signaled(signal) => { 37 | write!(f, "The command was terminated from signal {}", signal) 38 | } 39 | TerminationStatus::TerminatedNormally(exit_code) => write!( 40 | f, 41 | "The command terminated normally with exit code {}", 42 | exit_code 43 | ), 44 | } 45 | } 46 | } 47 | 48 | impl From for TerminationStatus { 49 | fn from(exit_status: ExitStatus) -> Self { 50 | match exit_status.signal() { 51 | Some(signal) => TerminationStatus::Signaled(signal), 52 | None => { 53 | // Safety: the docs state that, on Unix, ExitStatus::code will only fail if 54 | // the process was killed from a signal. We've just checked that this is not the case. 55 | let exit_code = exit_status.code().unwrap(); 56 | TerminationStatus::TerminatedNormally(exit_code) 57 | } 58 | } 59 | } 60 | } 61 | 62 | /// Simple abstraction over [`Command`] that spawns the command given by `args`, waits for its exit and returns a [`TerminationStatus`]. 63 | pub fn run_command<'a>(mut args: impl Iterator) -> Result { 64 | let command_name = args.next().ok_or(Error::NoCommandToRun)?; 65 | 66 | let mut child = Command::new(command_name).args(args).spawn()?; 67 | 68 | let exit_status = child.wait()?; 69 | 70 | Ok(exit_status.into()) 71 | } 72 | -------------------------------------------------------------------------------- /src/const_time.rs: -------------------------------------------------------------------------------- 1 | use memsec::memeq; 2 | 3 | /// A wrapper over a byte slice whose comparisons _should_ 4 | /// be immune to timing attacks 5 | pub struct VolatileBytes<'a> { 6 | bytes: &'a [u8], 7 | } 8 | 9 | impl<'a> VolatileBytes<'a> { 10 | /// Wraps over a byte slice 11 | /// 12 | /// Panics if the byte slice is empty 13 | pub fn new(bytes: &'a [u8]) -> Self { 14 | assert!(!bytes.is_empty()); 15 | Self { bytes } 16 | } 17 | 18 | /// Returns the amount of bytes in the inner slice 19 | pub fn len(&self) -> usize { 20 | self.bytes.len() 21 | } 22 | } 23 | 24 | impl AsRef<[u8]> for VolatileBytes<'_> { 25 | fn as_ref(&self) -> &[u8] { 26 | self.bytes 27 | } 28 | } 29 | 30 | impl PartialEq for VolatileBytes<'_> { 31 | fn eq(&self, other: &Self) -> bool { 32 | // TODO: slice length checking is O(1) so this likely cannot 33 | // create a timing attack, but I need to investigate further in order to assure that 34 | if self.len() != other.len() { 35 | return false; 36 | } 37 | 38 | let length = self.len(); 39 | 40 | unsafe { memeq(self.as_ref().as_ptr(), other.as_ref().as_ptr(), length) } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/crypt.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | 3 | use libc::c_char; 4 | use unixstring::UnixString; 5 | 6 | extern "C" { 7 | fn crypt(key: *const c_char, salt: *const c_char) -> *const c_char; 8 | } 9 | 10 | pub fn encrypt(key: impl AsRef, salt: impl AsRef) -> UnixString { 11 | let (key, salt) = (key.as_ref(), salt.as_ref()); 12 | 13 | let encrypted_ptr = unsafe { crypt(key.as_ptr(), salt.as_ptr()) }; 14 | 15 | unsafe { UnixString::from_ptr(encrypted_ptr) } 16 | } 17 | -------------------------------------------------------------------------------- /src/drop_zeroed.rs: -------------------------------------------------------------------------------- 1 | use std::{ptr, sync::atomic}; 2 | 3 | use unixstring::UnixString; 4 | 5 | pub trait DropZeroed { 6 | fn drop_zeroed(self); 7 | } 8 | 9 | impl DropZeroed for Vec { 10 | fn drop_zeroed(self) { 11 | let mut bytes = self; 12 | for byte in &mut bytes { 13 | unsafe { ptr::write_volatile(byte as *mut _, 0_u8) } 14 | atomic::compiler_fence(atomic::Ordering::SeqCst); 15 | } 16 | } 17 | } 18 | 19 | impl DropZeroed for String { 20 | fn drop_zeroed(self) { 21 | self.into_bytes().drop_zeroed(); 22 | } 23 | } 24 | 25 | impl DropZeroed for UnixString { 26 | fn drop_zeroed(self) { 27 | self.into_bytes().drop_zeroed() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/errno.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | use libc::{self, c_int}; 3 | 4 | cfg_if! { 5 | if #[cfg(target_os = "android")] { 6 | unsafe fn _errno() -> *mut c_int { 7 | libc::__errno() 8 | } 9 | } else if #[cfg(target_os = "linux")] { 10 | unsafe fn _errno() -> *mut c_int { 11 | libc::__errno_location() 12 | } 13 | } 14 | } 15 | 16 | pub fn errno() -> i32 { 17 | unsafe { (*_errno()) as i32 } 18 | } 19 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub enum Error { 5 | #[error("No command was supplied to be executed")] 6 | NoCommandToRun, 7 | #[error("IO error: {0}")] 8 | Io(#[from] io::Error), 9 | #[error("Failed to query password bank")] 10 | PasswordBank, 11 | #[error("Failed to query shadow file")] 12 | ShadowFile, 13 | #[error("Failed to ask for password")] 14 | PasswordAsking, 15 | #[error("Authentication failed")] 16 | Authentication, 17 | #[error("Failed to set UID")] 18 | Setuid, 19 | #[error("Some or all of the memory identified by mlockall could not be locked")] 20 | CouldNotLockMemory, 21 | #[error("The flags argument is zero, or includes unimplemented flags")] 22 | InvalidFlags, 23 | #[error("Could not lock the needed amount of memory")] 24 | TooMuchMemoryToLock, 25 | #[error("Not enough permissions to lock the memory pages")] 26 | NoPermission, 27 | #[error("Some unknown (or impossible) mlockall error happened")] 28 | UnknownMlockall, 29 | } 30 | 31 | pub type Result = std::result::Result; 32 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use unixstring::UnixString; 2 | 3 | use crate::drop_zeroed::DropZeroed; 4 | 5 | /// Safely reads the password from a terminal 6 | pub fn ask_for_password(username: impl AsRef) -> Option { 7 | println!("[kindly] Password for {}:", username.as_ref()); 8 | 9 | let password = rpassword::read_password_from_tty(None).ok()?; 10 | 11 | // As pointed out by @ferrouille, we must not use `UnixString::try_from` to convert here 12 | // since it could lead to a copy of the password being left unzeroed somewhere in the memory. 13 | let mut unx = UnixString::with_capacity(password.len()); 14 | 15 | let push_worked = unx.push_bytes(password.as_bytes()).is_ok(); 16 | 17 | password.drop_zeroed(); 18 | 19 | if push_worked { 20 | Some(unx) 21 | } else { 22 | unx.drop_zeroed(); 23 | None 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod command; 2 | mod const_time; 3 | mod crypt; 4 | mod drop_zeroed; 5 | mod errno; 6 | mod error; 7 | mod input; 8 | mod memory_lock; 9 | mod password_bank; 10 | 11 | use command::run_command; 12 | use error::{Error, Result}; 13 | use libc::setuid; 14 | use memory_lock::lock_memory_pages; 15 | use password_bank::{effective_user_id, PasswordBank}; 16 | 17 | use crate::{const_time::VolatileBytes, drop_zeroed::DropZeroed, input::ask_for_password}; 18 | 19 | fn try_main() -> Result { 20 | let mut args = argv::iter().skip(1).peekable(); 21 | let no_args_passed = args.peek().is_none(); 22 | 23 | if no_args_passed { 24 | return Err(Error::NoCommandToRun); 25 | } 26 | 27 | let effective_user_id = effective_user_id(); 28 | 29 | // Locks all pages mapped into the address space of the calling process. 30 | lock_memory_pages()?; 31 | 32 | // We'll query the password bank (/etc/passwd) for the entry corresponding to the user 33 | // that started `kindly` 34 | let (uid, mut pw_entry) = PasswordBank::query_password_entry()?; 35 | 36 | if pw_entry.password_is_one_char() { 37 | // When the password is one-char long (typically 'x'), that means that the actual 38 | // encrypted password is located in `/etc/shadow` instead of `/etc/passwd`. 39 | 40 | // We'll query the shadow file by username in order to get the actual encrypted password of the user 41 | // that started kindly 42 | pw_entry = PasswordBank::query_shadow_file_by_username(pw_entry.username_ptr())?; 43 | } 44 | 45 | // Asks for the user's password 46 | let password = ask_for_password(pw_entry.username_utf8()).ok_or(Error::PasswordAsking)?; 47 | 48 | // Encrypts the password in order to match the encrypted password entry in the password bank or shadow file 49 | let encrypted = crypt::encrypt(&password, pw_entry.password()); 50 | 51 | // Zeroes the password in-memory and drops it 52 | password.drop_zeroed(); 53 | 54 | let passwords_match = { 55 | // The user-supplied password that is now encrypted 56 | let encrypted = VolatileBytes::new(encrypted.as_bytes()); 57 | 58 | // The encrypted password value found in the password bank or in the shadow file 59 | let password_from_entry = VolatileBytes::new(pw_entry.password_bytes()); 60 | 61 | // We'll compare them through a "secure" `memeq` implementation 62 | encrypted == password_from_entry 63 | }; 64 | 65 | // The supplied password is not correct! 66 | if !passwords_match { 67 | return Err(Error::Authentication); 68 | } 69 | 70 | // Elevate the privileges of the running process 71 | if unsafe { setuid(effective_user_id) } != 0 { 72 | return Err(Error::Setuid); 73 | } 74 | 75 | // Runs the command given through command-line arguments and waits 76 | // for it to exit 77 | let status = run_command(args)?; 78 | 79 | // Deescalate the privileges of the running process 80 | if unsafe { setuid(uid) } != 0 { 81 | return Err(Error::Setuid); 82 | } 83 | 84 | if !status.is_ok() { 85 | // The spawned process was signaled or terminated normally with a non-zero exit code 86 | println!("[kindly] {}", status); 87 | } 88 | 89 | Ok(status.code_or_signal()) 90 | } 91 | 92 | fn main() { 93 | match try_main() { 94 | Ok(code_or_signal) => std::process::exit(code_or_signal), 95 | Err(err) => { 96 | println!("[kindly] {}", err); 97 | std::process::exit(127); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/memory_lock.rs: -------------------------------------------------------------------------------- 1 | use libc::{c_int, mlockall}; 2 | use libc::{EAGAIN, EINVAL, ENOMEM, EPERM}; 3 | use libc::{MCL_CURRENT, MCL_FUTURE}; 4 | 5 | use crate::errno::errno; 6 | use crate::error::{Error, Result}; 7 | 8 | extern "C" { 9 | pub static _MCL_ONFAULT: libc::c_int; 10 | } 11 | 12 | pub fn _mlockall_wrapper(flags: c_int) -> Result<()> { 13 | // Safety: mlockall is safe 14 | let err = unsafe { mlockall(flags) }; 15 | if err == 0 { 16 | return Ok(()); 17 | } 18 | 19 | // If err != 0, errno was set to describe the error that mlockall had 20 | Err(match errno() { 21 | // Some or all of the memory identified by the operation could not be locked when the call was made. 22 | EAGAIN => Error::CouldNotLockMemory, 23 | // The flags argument is zero, or includes unimplemented flags. 24 | EINVAL => Error::InvalidFlags, 25 | 26 | // Locking all of the pages currently mapped into the address space of the process 27 | // would exceed an implementation-defined limit on the amount of memory 28 | // that the process may lock. 29 | ENOMEM => Error::TooMuchMemoryToLock, 30 | // The calling process does not have appropriate privileges to perform the requested operation 31 | EPERM => Error::NoPermission, 32 | // Should not happen 33 | _ => Error::UnknownMlockall, 34 | }) 35 | } 36 | 37 | /// Locks all pages mapped into the address space of the calling process. 38 | pub fn lock_memory_pages() -> Result<()> { 39 | #[allow(non_snake_case)] 40 | let MCL_ONFAULT: c_int = unsafe { _MCL_ONFAULT }; 41 | match _mlockall_wrapper(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) { 42 | Err(err) => { 43 | eprintln!("First try at mlockall failed: {:?}", err); 44 | } 45 | Ok(_) => return Ok(()), 46 | } 47 | 48 | _mlockall_wrapper(MCL_CURRENT | MCL_FUTURE) 49 | } 50 | -------------------------------------------------------------------------------- /src/password_bank.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::CStr, marker::PhantomData, ptr::NonNull}; 2 | 3 | use libc::{c_char, getpwuid, getspnam}; 4 | 5 | use crate::error::{Error, Result}; 6 | 7 | /// Gets the user ID of the calling user 8 | fn calling_user_id() -> u32 { 9 | // Safety: the POSIX Programmer's Manual states that 10 | // getuid will always be successful. 11 | unsafe { libc::getuid() } 12 | } 13 | 14 | pub fn effective_user_id() -> u32 { 15 | // Safety: the POSIX Programmer's Manual states that 16 | // geteuid will always be successful. 17 | unsafe { libc::geteuid() } 18 | } 19 | 20 | /// A reduced view of an entry in the password bank or in the shadow file 21 | pub struct PasswordEntry<'a> { 22 | username: NonNull, 23 | password: NonNull, 24 | spook: PhantomData<&'a c_char>, 25 | } 26 | 27 | /// Abstracts over access to the password bank or to the shadow file 28 | pub struct PasswordBank; 29 | 30 | impl PasswordBank { 31 | /// Queries the password bank (/dev/passwd) through the effective user id of the caller. 32 | /// 33 | /// Returns an entry with the username and password 34 | pub fn query_password_entry() -> Result<(u32, PasswordEntry<'static>)> { 35 | let uid = calling_user_id(); 36 | 37 | let passwd = unsafe { getpwuid(uid) }; 38 | 39 | if !passwd.is_null() { 40 | // If getpwuid succeeded, let's get our data from it 41 | 42 | // Safety: we just checked that `passwd` is not NULL 43 | let uid = unsafe { (*passwd).pw_uid }; 44 | let pw_name = unsafe { (*passwd).pw_name }; 45 | let pw_passwd = unsafe { (*passwd).pw_passwd }; 46 | 47 | let password_entry = 48 | PasswordEntry::from_ptrs(pw_name, pw_passwd).ok_or(Error::PasswordBank)?; 49 | 50 | return Ok((uid, password_entry)); 51 | } 52 | 53 | Err(Error::PasswordBank) 54 | } 55 | 56 | pub fn query_shadow_file_by_username( 57 | username: NonNull, 58 | ) -> Result> { 59 | let shadow_entry = unsafe { getspnam(username.as_ptr()) }; 60 | 61 | if !shadow_entry.is_null() { 62 | // If getpwnam succeeded, let's get our data from it 63 | 64 | // Safety: we just checked that `passwd` is not NUlL 65 | let username = unsafe { (*shadow_entry).sp_namp }; 66 | let password = unsafe { (*shadow_entry).sp_pwdp }; 67 | 68 | return PasswordEntry::from_ptrs(username, password).ok_or(Error::ShadowFile); 69 | } 70 | 71 | Err(Error::ShadowFile) 72 | } 73 | } 74 | 75 | impl<'a> PasswordEntry<'a> { 76 | /// Instantiates a [`PasswordEntry`] from the raw pointers representing the username and the password 77 | pub fn from_ptrs(username: *const c_char, password: *const c_char) -> Option { 78 | Some(Self { 79 | username: NonNull::new(username as *mut _)?, 80 | password: NonNull::new(password as *mut _)?, 81 | spook: PhantomData, 82 | }) 83 | } 84 | 85 | /// Returns the raw pointer of the username of this entry 86 | pub fn username_ptr(&self) -> NonNull { 87 | self.username 88 | } 89 | 90 | /// Returns the username of this entry as a [`CStr`] 91 | pub fn username(&self) -> &'_ CStr { 92 | unsafe { CStr::from_ptr(self.username.as_ptr()) } 93 | } 94 | 95 | /// Returns the username of this entry in valid UTF-8 96 | pub fn username_utf8(&self) -> Cow<'_, str> { 97 | self.username().to_string_lossy() 98 | } 99 | 100 | /// Returns the password of this entry as a [`CStr`] 101 | pub fn password(&self) -> &'_ CStr { 102 | unsafe { CStr::from_ptr(self.password.as_ptr()) } 103 | } 104 | 105 | /// Returns a byte slice of the password of this entry 106 | pub fn password_bytes(&self) -> &'_ [u8] { 107 | self.password().to_bytes() 108 | } 109 | 110 | /// Returns true if the password is one char in length 111 | pub fn password_is_one_char(&self) -> bool { 112 | self.password_bytes().len() == 1 113 | } 114 | } 115 | --------------------------------------------------------------------------------