diff --git a/.gitignore b/.gitignore index dc73d83..0f0d9c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock test.sh +crab.tar.gz diff --git a/Cargo.lock b/Cargo.lock index 5ba42db..054acb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,13 +34,12 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "crab" -version = "0.0.5" +version = "0.0.6" dependencies = [ "exec", "nix", "pam", "procinfo", - "pwd", "rpassword", "serde_json", "time", @@ -144,15 +143,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "proc-macro2" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" -dependencies = [ - "unicode-ident", -] - [[package]] name = "procinfo" version = "0.4.2" @@ -165,25 +155,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "pwd" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c71c0c79b9701efe4e1e4b563b2016dd4ee789eb99badcb09d61ac4b92e4a2" -dependencies = [ - "libc", - "thiserror", -] - -[[package]] -name = "quote" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" -dependencies = [ - "proc-macro2", -] - [[package]] name = "rpassword" version = "7.1.0" @@ -241,37 +212,6 @@ dependencies = [ "serde", ] -[[package]] -name = "syn" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "time" version = "0.3.17" @@ -288,12 +228,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" -[[package]] -name = "unicode-ident" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" - [[package]] name = "users" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 0408f28..e9b9a13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,10 @@ [package] name = "crab" -version = "0.0.5" +version = "0.0.6" edition = "2021" [dependencies] pam = "0.7.0" -pwd = "1.4.0" nix = "0.25.0" exec = "0.3.1" rpassword = "7.1.0" diff --git a/conf b/conf deleted file mode 100644 index bebb505..0000000 --- a/conf +++ /dev/null @@ -1 +0,0 @@ -root true \ No newline at end of file diff --git a/config/default b/config/default new file mode 100644 index 0000000..8976cc6 --- /dev/null +++ b/config/default @@ -0,0 +1,5 @@ +deny :docker +permit nopass persist linus as root +#deny stallman +permit :wheel persist +permit nvidia as fu \ No newline at end of file diff --git a/pam b/config/pam similarity index 100% rename from pam rename to config/pam diff --git a/deployments/aur/.SRCINFO b/deployments/aur/.SRCINFO index 66c6ce5..83827ea 100644 --- a/deployments/aur/.SRCINFO +++ b/deployments/aur/.SRCINFO @@ -1,16 +1,15 @@ pkgbase = crab pkgdesc = A rusty permission authentication system - pkgver = 0.0.5 + pkgver = 0.0.6 pkgrel = 2 - url = https://g.tylerm.dev/tylermurphy534/crab.git + url = https://g.tylerm.dev/tylermurphy534/crab arch = x86_64 arch = i686 license = GPL3 - makedepends = git makedepends = cargo depends = glibc depends = pam - source = git+https://g.tylerm.dev/tylermurphy534/crab.git + source = crab-0.0.6.tar.gz::https://f.tylerm.dev/source/crab/crab-0.0.6.tar.gz md5sums = SKIP pkgname = crab diff --git a/deployments/aur/PKGBUILD b/deployments/aur/PKGBUILD index b49ce75..2edfc1e 100644 --- a/deployments/aur/PKGBUILD +++ b/deployments/aur/PKGBUILD @@ -1,24 +1,24 @@ # Maintainer: Tyler Murphy pkgname=crab -pkgver=0.0.5 -pkgrel=2 +pkgver=0.0.6 +pkgrel=1 pkgdesc="A rusty permission authentication system" arch=('x86_64' 'i686') -url="https://g.tylerm.dev/tylermurphy534/crab.git" +url="https://g.tylerm.dev/tylermurphy534/crab" license=('GPL3') -makedepends=('git' 'cargo') +makedepends=('cargo') depends=('glibc' 'pam') -source=("git+$url") +source=("$pkgname-$pkgver.tar.gz::https://f.tylerm.dev/source/$pkgname/$pkgname-$pkgver.tar.gz") md5sums=('SKIP') build() { - cd crab + cd "$srcdir" cargo build --release } package() { - cd crab - install -D --mode=6755 --owner=root --group=root ./target/release/crab ${pkgdir}/usr/bin/crab - install -D --mode=660 --owner=root --group=root pam ${pkgdir}/etc/pam.d/crab - install -D --mode=660 --owner=root --group=root conf ${pkgdir}/usr/share/crab/crab.conf + cd "$srcdir" + install -D --mode=4755 --owner=root --group=root ./target/release/crab ${pkgdir}/usr/bin/crab + install -D --mode=600 --owner=root --group=root ./config/pam ${pkgdir}/etc/pam.d/crab + install -D --mode=644 --owner=root --group=root ./config/default ${pkgdir}/usr/share/crab/crab.conf } diff --git a/deployments/source/install.sh b/deployments/source/install.sh new file mode 100755 index 0000000..b488693 --- /dev/null +++ b/deployments/source/install.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +if [[ $(/usr/bin/id -u) -ne 0 ]]; then + echo "Please run this as root" + exit +fi + +# Copy executable to bin +cp ./target/release/crab /usr/bin/crab +chown root:root /usr/bin/crab +chmod 4755 /usr/bin/crab + +# Set up config files +cp ./config/pam /etc/pam.d/crab +chmod 600 /etc/pam.d/crab + +mkdir /usr/share/crab +chmod 644 /usr/share/crab + +cp ./config/default /usr/share/crab/crab.conf +chmod 644 /usr/share/crab/crab.conf diff --git a/deployments/source/uninstall.sh b/deployments/source/uninstall.sh new file mode 100755 index 0000000..cf7ade8 --- /dev/null +++ b/deployments/source/uninstall.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +if [[ $(/usr/bin/id -u) -ne 0 ]]; then + echo "Please run this as root" + exit +fi + +# Delete crab files +rm /etc/pam.d/crab +rm /usr/bin/crab +rm -fr /usr/share/crab diff --git a/install.sh b/install.sh deleted file mode 100755 index 716d9b1..0000000 --- a/install.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -if [[ $(/usr/bin/id -u) -ne 0 ]]; then - echo "Please run this as root" - exit -fi - -# Copy executable to bin -cp ./target/release/crab /usr/bin/crab -chown root:root /bin/crab -chmod 6755 /bin/crab - -# Set up config files -cp pam /etc/pam.d/crab -mkdir /usr/share/crab -cp conf /usr/share/crab/crab.conf -chmod 660 /usr/share/crab/crab.conf diff --git a/readme.md b/readme.md index 971c469..e191759 100644 --- a/readme.md +++ b/readme.md @@ -3,20 +3,47 @@ # Installation ### From Source -First run `cargo build --release` to compile the binary. -Then run `install.sh` as root to install crab. +To be able to build the package, you need cargo wich you can get either though rust or rust up. -Run `uninstall.sh` as root to uninstall crab. +To build, run the following commands below in the root directory of the repo. Make sure to run the shell script as root. +```bash +cargo build --release +./deployments/source/install.sh +``` + +To uninstall, just run the following script as root. +```bash +./deployments/source/uninstall.sh +``` ### Arch Based Systems If you are on an arch based distro, crab is avaliable on the [AUR](https://aur.archlinux.org/packages/crab) as `crab`. +``` +paru -S crab +``` # Configuration -Crab supports multiple users with persistence. Each line of the config is the username, then `true` of `false` if the crab authentication persists. + +The default configuration file is stored in `/usr/share/crab/crab.conf` and must be coppied to `/etc/crab.conf`. +```bash +cp /usr/share/crab/crab.conf /etc/crab.conf +chown root:root /etc/crab.conf +chmod 600 /etc/crab.conf +``` + +Each line in the configuration specifies a different rule. Each rule is applied from top to bottom, +so the first onethat matches a user is what is used. The first word is either `permit` or `deny` to +allow or deny a certain group. Then the tags `persist` and `nopass` can be added to allow authoriziation +persistance or skipping respectively. Then a user can be specified by putting their name, or a group by a +colon then the groups name. Finally, if you dont want to run that user as root, you can add `as` and then +a user name to run the process as. All lines starting in a # will be ignored. For Example ``` -root true -tylerm false +deny :docker +permit nopass persist linus as root +#deny stallman +permit :wheel persist +permit nvidia as fu ``` -The default configuration file is stored in `/usr/share/crab/crab.conf` and must be coppied to `/etc/crab.conf`. +Please make sure when editing your config that not normal users can edit the file, but only root. If normal users can edit the config, they can add themselvs as permitted and get elevated privilages. diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..99a8216 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,264 @@ +use nix::unistd::{User, Group, Uid, Gid, self}; +use crate::persist; + + +pub struct Config { + pub permit: bool, + pub persist: bool, + pub nopass: bool, + pub user_uid: Option, + pub user_gid: Option, + pub privlaged_uid: Uid, +} + + +/// Returns a option containing the config ad the specific pile path. +/// This function will print out config syntax errors to the standard +/// output if it failes to parse certain lines. +/// #### Arguments +/// * `path` - The path to the config file +/// #### Returns +/// * `None` - If the config failed to load +/// * `Some(Vec)` - If the config was sucessfully parsed. +pub fn load_config_file(path: &str) -> Option> { + let file = match std::fs::read_to_string(path) { + Err(e) => { + eprintln!("{}: {}", &path, e); + return None + }, + Ok(data) => data + }; + + let mut configs = vec![]; + + for (line_num, line) in file.split("\n").enumerate() { + + let args: Vec<&str> = line.split(" ").collect(); + let len = args.len(); + + if line.starts_with("#") || line.trim() == "" { + continue; + } + + if len < 2 { + config_error(line_num, "Not enough arguments"); + continue; + } + + let permit = match args[0] { + "permit" => true, + "deny" => false, + _ => { + config_error(line_num, "The first argument must be `permit` or `deny"); + continue; + } + }; + + let (user_name, privlaged_name, name_index) = match args.iter().position(|&a| a == "as") { + Some(index) => { + if index != len - 2 { + config_error(line_num, "Target user not specified or to many arguments after `as`"); + continue; + } + (args[index-1].to_string(), args[index+1].to_string(), index-1) + }, + None => (args[len-1].to_string(), "root".to_string(), len-1) + }; + + let persist = args[1..name_index].contains(&"persist"); + + let nopass = args[1..name_index].contains(&"nopass"); + + + for &check in args[1..name_index].iter() { + match check { + "persist" => continue, + "nopass" => continue, + _ => { + config_error(line_num, &format!("Unexpected token `{}`", check)) + } + } + } + + + let (user_uid, user_gid) = + if user_name.starts_with(":") { + match get_gid_from_name(&user_name[1..]) { + Some(gid) => (None, Some(gid)), + None => { + config_error(line_num, &format!("Group `{}` does not exist", &user_name[1..])); + continue; + } + } + } else { + match get_uid_from_name(&user_name) { + Some(uid) => (Some(uid), None), + None => { + config_error(line_num, &format!("User `{}` does not exist", user_name)); + continue; + } + } + }; + + let privlaged_uid = match get_uid_from_name(&privlaged_name) { + Some(uid) => uid, + None => { + config_error(line_num, &format!("User `{}` does not exist", privlaged_name)); + continue; + } + }; + + configs.push(Config { + permit, + persist, + nopass, + user_uid, + user_gid, + privlaged_uid + }); + } + Some(configs) +} + + +/// Print a crab config error to the standard output +fn config_error(line_num: usize, message: &str) { + eprintln!("Error in config at line {}: {}", line_num, message); +} + + +/// Returns a Uid from a Users name +/// #### Arguments +/// * `name` - The name of the user +/// #### Returns +/// * `None` - If the user doesn't exist +/// * `Some(Gid)` - If the user exists +fn get_uid_from_name(name: &str) -> Option { + return match User::from_name(name) { + Ok(result) => match result { + Some(data) => Some(data.uid), + None => None + }, + Err(_) => None + } +} + + +/// Returns a Uesrs name from a Uid +/// #### Arguments +/// * `uid` - The uid of the user +/// #### Returns +/// * `None` - If the user doesn't exist +/// * `Some(Gid)` - If the user exists +fn get_name_from_uid(uid: Uid) -> Option { + return match User::from_uid(uid) { + Ok(result) => match result { + Some(data) => Some(data.name), + None => None + }, + Err(_) => None + } +} + + +/// Returns a Gid from a Groups name +/// #### Arguments +/// * `name` - The name of the group +/// #### Returns +/// * `None` - If the group doesn't exist +/// * `Some(Gid)` - If the group exists +fn get_gid_from_name(name: &str) -> Option { + return match Group::from_name(name) { + Ok(result) => match result { + Some(data) => Some(data.gid), + None => None + }, + Err(_) => None + } +} + + +/// Returns a vector of group names of the groups the current effective uid is in +/// #### Returns +/// A vector of strings of the groups the user is in. If the vector is empty, +/// either the function coudn't retrieve the users groups, or the user is not in +/// any groups. +fn get_groups() -> Vec { + let groups = match unistd::getgroups() { + Ok(data) => data, + Err(_) => return vec![] + }; + groups +} + + +/// Returns if the user is authorized given a sepcific config and user name. +/// #### Arguments +/// * `config` - A config struct contaning the authorization settings for crab +/// * `uid` - The uid to check is authorized +/// #### Returns +/// * `None` - If the uid is not authorized +/// * `Some(Config)` - If the uid is authorized, returns the specific index of the associated config +pub fn authorize(configs: &Vec, uid: Uid) -> Option { + let groups = get_groups(); + for (config_index, config) in configs.iter().enumerate() { + if config.user_gid.is_some() { + if groups.contains(&config.user_gid.unwrap()) { + if config.permit { + return Some(config_index) + } else { + return None + } + } + } else if config.user_uid.is_some() { + if config.user_uid.unwrap() == uid { + if config.permit { + return Some(config_index) + } else { + return None + } + } + } + } + None +} + + +/// Authenticates an authorized user for the current session. +/// If the user is already persisted, it will attempt to +/// read the persist file, and then skip authentication if +/// the is still persisted. +/// #### Arguments +/// * `config` - The config associated with the user to authenticate with +/// * `force_pass` - Force a password prompt even if persistance is set to true +/// * `uid` - The user uid that is authenticating +/// #### Returns +/// * `true` - If the user authenticated sucessfully +/// * `false` - If the user failed to authenticate +pub fn authenticate(config: &Config, force_pass: bool, uid: Uid) -> bool { + let name = match get_name_from_uid(uid) { + Some(data) => data, + None => return false + }; + if config.nopass || ( !force_pass && config.persist && persist::get_persist(&name) ) { + return true; + } + let input = match rpassword::prompt_password(format!("crab ({}) password: ", &name)) { + Ok(data) => data, + Err(_) => return false + }; + let mut auth = match pam::Authenticator::with_password("crab") { + Ok(data) => data, + Err(_) => return false + }; + auth.get_handler().set_credentials(&name, input); + if !auth.authenticate().is_ok() || !auth.open_session().is_ok() { + return false; + } + if !force_pass && config.persist { + persist::set_persist(&name); + } else if force_pass { + persist::remove_persist(&name); + } + return true; +} diff --git a/src/flags.rs b/src/flags.rs index 9432b8f..a945483 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -1,66 +1,92 @@ -pub struct Flags { - pub help: bool, - pub version: bool, - pub dont_persist: bool, - pub arg_count: usize -} - -pub fn parse(args: &[String]) -> Option { - let mut flags = Flags { - help: false, - version: false, - dont_persist: false, - arg_count: 0 - }; - for arg in args { - if !is_arg(&arg) { break; } - flags.arg_count += 1; - if arg.starts_with("--") { - let flag = &arg[2..]; - if !set_flag(&flag, &mut flags) { - eprintln!("Invalid argument: {}", arg); - return None - } - } else { - let flag = &arg[1..]; - for char in flag.chars() { - if !set_flag(&char.to_string(), &mut flags) { - eprintln!("Invalid argument: {}", arg); - return None - } - } - } - } - Some(flags) -} - -fn is_arg(arg: &str) -> bool { - return arg.starts_with("-"); -} const HELP_FLAG: &str = "help h"; const VERSION_FLAG: &str = "version v"; const DONT_PERSIST: &str = "d"; -fn set_flag(arg: &str, flags: &mut Flags) -> bool { - if has_flag_set(&arg, HELP_FLAG) { - flags.help = true; - return true - } else if has_flag_set(&arg, VERSION_FLAG) { - flags.version = true; - return true - } else if has_flag_set(&arg, DONT_PERSIST) { - flags.dont_persist = true; - return true - } - false + +pub struct Flags { + pub help: bool, + pub version: bool, + pub dont_persist: bool, + pub arg_count: usize } -fn has_flag_set(arg: &str, check: &str) -> bool { - for check_arg in check.split(" ") { - if check_arg == arg { - return true + +/// Parses an list of String arguments into a set of flags that can be used by crab +/// #### Arguments +/// * `args` - The list of String arguments +/// #### Returns +/// * `None` - If there is an invalid argument in the list +/// * `Some(Flags)` - If the arguments were secussfully parsed, returning the flags +pub fn parse(args: &[String]) -> Option { + let mut flags = Flags { + help: false, + version: false, + dont_persist: false, + arg_count: 0 + }; + for arg in args { + if !is_arg(&arg) { break; } + flags.arg_count += 1; + if arg.starts_with("--") { + let flag = &arg[2..]; + if !set_flag(&flag, &mut flags) { + eprintln!("Invalid argument: {}", arg); + return None + } + } else { + let flag = &arg[1..]; + for char in flag.chars() { + if !set_flag(&char.to_string(), &mut flags) { + eprintln!("Invalid argument: {}", arg); + return None + } + } + } } - } - return false + Some(flags) +} + + +/// Checks if a given string is a given argument +fn is_arg(arg: &str) -> bool { + return arg.starts_with("-"); +} + + +/// Sets a flag in a `Flags` struct +/// #### Arguments +/// * `arg` - The argument to check +/// * `flags` - The `Flags` stuct to update +/// #### Returns +/// * `true` - If the argument passed is a valid flag +/// * `false` - If the argument passed is not a valid flag +fn set_flag(arg: &str, flags: &mut Flags) -> bool { + if has_flag_set(&arg, HELP_FLAG) { + flags.help = true; + return true + } else if has_flag_set(&arg, VERSION_FLAG) { + flags.version = true; + return true + } else if has_flag_set(&arg, DONT_PERSIST) { + flags.dont_persist = true; + return true + } + false +} + + +/// Returns if a given argument is a certain flag +/// #### Arguments +/// * `arg` - The arch to check +/// #### Returns +/// * `true` - If the argument matches the flag +/// * `false` - If the argument doesn't match the flag +fn has_flag_set(arg: &str, check: &str) -> bool { + for check_arg in check.split(" ") { + if check_arg == arg { + return true + } + } + return false } diff --git a/src/help.rs b/src/help.rs deleted file mode 100644 index e6f4a72..0000000 --- a/src/help.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub fn help() { - let help = -"Usage: - crab [-d] command [args] -Options: - -v --version Get the current version of the package - -h --help Generates the crab help message - -d If your user is set to persist, dont save persistance"; - println!("{}", help); -} diff --git a/src/main.rs b/src/main.rs index 28f08cd..56df611 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,142 +1,100 @@ use std::env; use std::process::ExitCode; -use pwd::Passwd; -use nix::unistd; -extern crate time; -const ERROR_ARGS: u8 = 1; -const ERROR_CONFIG: u8 = 2; -const ERROR_NO_USER: u8 = 3; -const ERROR_NOT_AUTHORIZED: u8 = 4; -const ERROR_AUTH_FAILED: u8 = 5; -const ERROR_RUN_ROOT: u8 = 6; - -mod persist; +mod auth; mod flags; -mod help; +mod persist; mod secure; + +const ERROR_ARGS: u8 = 1; +const ERROR_CONFIG: u8 = 2; +const ERROR_NOT_AUTHORIZED: u8 = 3; +const ERROR_AUTH_FAILED: u8 = 4; +const ERROR_ELEVATE_PRIVILEGES: u8 = 5; + + fn main() -> ExitCode { + + // gets the arguments from the env let args: Vec = env::args().collect(); + + // pase the arguments into valid flags that are usuable by crab let flags = match flags::parse(&args[1..]) { Some(data) => data, + // if there is an invalid flag, print the help message and exit None => { - help::help(); + help(); return ExitCode::from(ERROR_ARGS); } }; + + // If the version arg flag is set, print the crab version if flags.version { - println!("crab version 0.0.5"); + println!("crab version 0.0.6"); return ExitCode::SUCCESS; } + + // If the help arg flag is set, print the crab help message if flags.help { - help::help(); + help(); return ExitCode::SUCCESS; } + + // If the amount of acutal command arguments is less than two, a.k.a just `crab`, print the command usage if args.len() - flags.arg_count < 2 { println!("usage: crab [-d] command [args]"); return ExitCode::SUCCESS; } - let config = match config("/etc/crab.conf") { + + // Load the command config from /etc + let configs = match auth::load_config_file("/etc/crab.conf") { Some(data) => data, None => return ExitCode::from(ERROR_CONFIG) }; - let user = match Passwd::current_user() { + + + // check if the user is authorized + let auth = match auth::authorize(&configs, nix::unistd::getuid()) { Some(data) => data, - None => { - eprintln!("You dont exist."); - return ExitCode::from(ERROR_NO_USER); - } - }; - let persist = match allowed(&config, &user.name) { - Some(data) => data && !flags.dont_persist, None => { eprintln!("Operation Not Permitted."); return ExitCode::from(ERROR_NOT_AUTHORIZED); } }; - if !validate(&user.name, persist) { + // authenticate the user + if !auth::authenticate(&configs[auth], flags.dont_persist, nix::unistd::getuid()) { eprintln!("Authentication failed."); return ExitCode::from(ERROR_AUTH_FAILED); } - if !unistd::setuid(unistd::geteuid()).is_ok() || !unistd::setgid(unistd::getegid()).is_ok() { - eprintln!("Failed to set root permissions"); - return ExitCode::from(ERROR_RUN_ROOT); + // elevate privileges + if nix::unistd::setuid(configs[auth].privlaged_uid).is_err() { + eprintln!("Failed to elevate privileges."); + return ExitCode::from(ERROR_ELEVATE_PRIVILEGES); }; + + // execute the passed command let start = 1 + flags.arg_count; let err = exec::execvp(&args[start], &args[start..]); + // print an error if an error was returned eprintln!("{}", err); ExitCode::SUCCESS } -struct Config { - users: Vec<(String, bool)> -} -fn validate(user: &str, persist: bool) -> bool { - if persist && persist::get_persist(user) { - return true; - } - let input = match rpassword::prompt_password(format!("crab ({}) password: ", user)) { - Ok(data) => data, - Err(_) => return false - }; - let mut auth = match pam::Authenticator::with_password("crab") { - Ok(data) => data, - Err(_) => return false - }; - auth.get_handler().set_credentials(user.to_owned(), input); - if !auth.authenticate().is_ok() || !auth.open_session().is_ok() { - return false; - } - if persist { - persist::set_persist(user); - } - return true; -} - -fn allowed(config: &Config, user: &str) -> Option { - for (name, persist) in &config.users { - if name == user { - return Some(persist.clone()); - } - } - None -} - -fn config(path: &str) -> Option { - let file = match std::fs::read_to_string(path) { - Err(e) => { - eprintln!("{}: {}", &path, e); - return None - }, - Ok(data) => data - }; - - let mut users = vec![]; - for (line_num, line) in file.split("\n").enumerate() { - let args: Vec<&str> = line.split(" ").collect(); - if line.trim() == "" { - continue; - } - if args.len() < 2 { - eprintln!("Error in config at line {}: Not enough arguments", line_num); - continue; - } - let user: String = args[0].to_string(); - let persist: bool = match args[1].parse() { - Err(e) => { - eprintln!("Error in config at line {}: {}", line_num, e); - continue; - }, - Ok(data) => data - }; - users.push((user, persist)); - } - Some(Config{users}) +/// Prints the help message to the standard output +fn help() { + let help = +"Usage: + crab [-d] command [args] +Options: + -v --version Get the current version of the package + -h --help Generates the crab help message + -d If your user is set to persist, dont save persistance"; + println!("{}", help); } diff --git a/src/persist.rs b/src/persist.rs index 6a813dc..2bca386 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -1,69 +1,116 @@ use std::time::SystemTime; use serde_json::Value; - use crate::secure; + const PERSIST_TIME: u64 = 60 * 3; const PERSIST_PATH: &str = "/var/run/crab"; + +/// Returns true or false if a user is currently persisted from +/// a prior authentication. If the persist file had been tampered +/// with, or not trusted, it will be ignored, and return false. +/// #### Arguments +/// * `user` - The user to check if is persisted +/// #### Returns +/// * `true` - If the user is persisted +/// * `false` - If the user is not persisted, or if the persist file is not trusted pub fn get_persist(user: &str) -> bool { - let json = match get_terminal_config() { - Some(data) => data, - None => return false - }; - let timestamp = match json[user].as_u64() { - Some(data) => data, - None => return false - }; - return now() - timestamp < PERSIST_TIME && timestamp - 1 < now(); + let json = match get_persist_config() { + Some(data) => data, + None => return false + }; + let timestamp = match json[user].as_u64() { + Some(data) => data, + None => return false + }; + return now() - timestamp < PERSIST_TIME && timestamp - 1 < now(); } + +/// Updates a user in the current sessions persist file +/// #### Arguments +/// * `user` - The user to set persisted pub fn set_persist(user: &str) { - let mut json = match get_terminal_config() { - Some(data) => data, - None => return - }; - json[user] = Value::from(now()); - let id = match get_terminal_process() { - Some(data) => data, - None => return - }; - match secure::write_file(PERSIST_PATH, &format!("{}", id), &json.to_string()) { - Ok(_) => {}, - Err(e) => { - eprintln!("Internal Error: {}", e) - } - }; + let mut json = match get_persist_config() { + Some(data) => data, + None => return + }; + json[user] = Value::from(now()); + let session = match get_current_session() { + Some(data) => data, + None => return + }; + match secure::write_file(PERSIST_PATH, &format!("{}", session), &json.to_string()) { + Ok(_) => {}, + Err(_) => { + eprintln!("crab: An internal error has occured"); + } + }; } -fn get_terminal_process() -> Option { - let id: i32 = match std::process::id().try_into() { - Ok(data) => data, - Err(_) => return None - }; - let stat = match procinfo::pid::stat(id) { - Ok(data) => data, - Err(_) => return None - }; - Some(stat.session) + +/// Removes a user from the current sessions persist file +/// #### Arguments +/// * `user` - The user to set non-persisted +pub fn remove_persist(user: &str) { + let mut json = match get_persist_config() { + Some(data) => data, + None => return + }; + json[user] = Value::from(0); + let session = match get_current_session() { + Some(data) => data, + None => return + }; + match secure::write_file(PERSIST_PATH, &format!("{}", session), &json.to_string()) { + Ok(_) => {}, + Err(_) => { + eprintln!("crab: An internal error has occured"); + } + }; } -fn get_terminal_config() -> Option { - let id = match get_terminal_process() { - Some(data) => data, - None => return None - }; - let data = match secure::read_file(PERSIST_PATH, &format!("{}", id)) { - Some(data) => data, - None => "{}".to_string() - }; - let json: Value = match serde_json::from_str(&data) { - Ok(data) => data, - Err(_) => return None - }; - Some(json) + +/// Gets the current session that crab is running in +/// #### Returns +/// * `None` - If crab failed to get the current session +/// * `Some(i32)` - If the session is retrieved, returns the i32/pid_t session id +fn get_current_session() -> Option { + let pid: i32 = match std::process::id().try_into() { + Ok(data) => data, + Err(_) => return None + }; + let pid_stat = match procinfo::pid::stat(pid) { + Ok(data) => data, + Err(_) => return None + }; + Some(pid_stat.session) } + +/// Gets the current persist file for the current session +/// #### Returns +/// * `None` - If the persist file is untrusted or doesnt exist +/// * `Some(Value)` - If the persist file is retrieved, returns the serde_json Value of the file +fn get_persist_config() -> Option { + let session = match get_current_session() { + Some(data) => data, + None => return None + }; + let data = match secure::read_file(PERSIST_PATH, &format!("{}", session)) { + Some(data) => data, + None => "{}".to_string() + }; + let json: Value = match serde_json::from_str(&data) { + Ok(data) => data, + Err(_) => return None + }; + Some(json) +} + + +// Gets the current time in seconds since the Unix Epoch fn now() -> u64 { - return SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + return SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); } diff --git a/src/secure.rs b/src/secure.rs index 1fc3a11..7c85ece 100644 --- a/src/secure.rs +++ b/src/secure.rs @@ -1,51 +1,84 @@ -use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io::ErrorKind}; -use nix::unistd; +use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io::{self, ErrorKind}, path::Path}; +use nix::unistd::{self, Uid, Gid}; -pub fn write_file(dir: &str, file: &str, data: &str) -> Result<(), Box> { - std::fs::create_dir_all(dir)?; - make_file_root(dir)?; - let path = path(dir, file); - std::fs::write(&path, "")?; - make_file_root(&path)?; - std::fs::write(&path, data)?; - Ok(()) + +/// Writes a file securly to a specified path with given data +/// #### Arguments +/// * `dir` - The directory of the secure folder +/// * `file` - The file name to write +/// * `data` - The data to write +/// #### Returns +/// A ``io::Result<()>`` if the write succeded or failed +pub fn write_file(dir: &str, file: &str, data: &str) -> Result<(), io::Error> { + fs::create_dir_all(dir)?; + set_file_permissions(0, 0, 0o100600, dir)?; + let path = path(dir, file); + fs::write(&path, data)?; + set_file_permissions(0, 0, 0o100600, &path)?; + Ok(()) } + +/// Reads the file secutly to a specified path. If the file has +/// been tampered (permissions have been changed), it ignores the +/// file. +/// #### Arguments +/// * `dir` - The directory of the secure folder +/// * `file` - The file name to write +/// #### Returns +/// * `None` - If the files doesnt exist or isnt trusted +/// * `Some(String) - If the file is trusted, it returns the file's contents pub fn read_file(dir: &str, file: &str) -> Option { - let path = path(dir,file); - if !is_file_root(&path) { - return None; - } - match std::fs::read_to_string(&path) { - Ok(data) => return Some(data), - Err(_) => return None - }; -} - -fn make_file_root(path: &str) -> Result<(), Box> { - unistd::chown(std::path::Path::new(path), Some(unistd::Uid::from(0)), Some(unistd::Gid::from(0)))?; - let metadata = std::fs::metadata(path)?; - let mut perms = metadata.permissions(); - perms.set_mode(0o100600); - fs::set_permissions(path, perms)?; - Ok(()) -} - -fn is_file_root(path: &str) -> bool { - return check_file_permissions(0, 0, 0o100600, path); -} - -fn check_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> bool { - let metadata = match std::fs::metadata(path) { - Ok(data) => data, - Err(e) => { - return e.kind() == ErrorKind::NotFound + let path = path(dir,file); + if !check_file_permissions(0, 0, 0o100600, &path) { + return None; } - }; - let perms = metadata.permissions(); - return perms.mode() == mode && metadata.st_uid() == uid && metadata.st_gid() == gid; + match fs::read_to_string(&path) { + Ok(data) => return Some(data), + Err(_) => return None + }; } + +/// Sets the permission for a secure file +/// #### Arguments +/// * `uid` - The user to own the file +/// * `gid` - The group to own the file +/// * `mode` - The mode permissions of the file +/// * `path` - The path of the secure file +/// #### Returns +/// A ``io::Result<()>`` if the write succeded or failed +fn set_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> Result<(), io::Error> { + unistd::chown(Path::new(path), Some(Uid::from(uid)), Some(Gid::from(gid)))?; + let metadata = fs::metadata(path)?; + let mut perms = metadata.permissions(); + perms.set_mode(mode); + fs::set_permissions(path, perms)?; + Ok(()) +} + + +/// Checks if the files permissions equals the given parameters +/// #### Arguments +/// * `uid` - The user to own the file +/// * `gid` - The group to own the file +/// * `mode` - The mode permissions of the file +/// * `path` - The path of the secure file +/// #### Returns +/// True or false if the files permissions match +fn check_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> bool { + let metadata = match fs::metadata(path) { + Ok(data) => data, + Err(e) => { + return e.kind() == ErrorKind::NotFound; + } + }; + let perms = metadata.permissions(); + return perms.mode() == mode && metadata.st_uid() == uid && metadata.st_gid() == gid; +} + + +/// Get the path of a file given a directory and file name fn path(dir: &str, file: &str) -> String { - return format!("{}/{}", dir, file); -} \ No newline at end of file + return format!("{}/{}.persist", dir, file); +} diff --git a/uninstall.sh b/uninstall.sh deleted file mode 100755 index f6cba7f..0000000 --- a/uninstall.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -if [[ $(/usr/bin/id -u) -ne 0 ]]; then - echo "Please run this as root" - exit -fi - -# Delete crab files -rm /usr/bin/crab -chown root:root /bin/crab -chmod 6755 /bin/crab - -# Set up config files -cp pam /etc/pam.d/crab -cp -n conf /etc/crab.conf -chmod 660 /etc/crab.conf