From f7a13253e72f1dde7389fb98c090b753fdfc42b9 Mon Sep 17 00:00:00 2001 From: tylermurphy534 Date: Thu, 10 Nov 2022 11:28:10 -0500 Subject: slight refactor --- src/persist.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/persist.rs') diff --git a/src/persist.rs b/src/persist.rs index 6a813dc..667b12a 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -21,7 +21,7 @@ pub fn get_persist(user: &str) -> bool { pub fn set_persist(user: &str) { let mut json = match get_terminal_config() { Some(data) => data, - None => return + None => serde_json::from_str("{}").unwrap() }; json[user] = Value::from(now()); let id = match get_terminal_process() { @@ -30,22 +30,22 @@ pub fn set_persist(user: &str) { }; match secure::write_file(PERSIST_PATH, &format!("{}", id), &json.to_string()) { Ok(_) => {}, - Err(e) => { - eprintln!("Internal Error: {}", e) + Err(_) => { + eprintln!("crab: An Internal Has Error") } }; } fn get_terminal_process() -> Option { - let id: i32 = match std::process::id().try_into() { + let pid: i32 = match std::process::id().try_into() { Ok(data) => data, Err(_) => return None }; - let stat = match procinfo::pid::stat(id) { + let pid_stat = match procinfo::pid::stat(pid) { Ok(data) => data, Err(_) => return None }; - Some(stat.session) + Some(pid_stat.session) } fn get_terminal_config() -> Option { -- cgit v1.2.3-freya From 83d045a58cd67286ae9d9fa2bee9103fa0cc931a Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Thu, 10 Nov 2022 14:17:16 -0500 Subject: documentation and group support --- src/flags.rs | 32 ++++++++++++++-- src/main.rs | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++------ src/persist.rs | 43 ++++++++++++++++----- src/secure.rs | 38 +++++++++++++++++++ 4 files changed, 204 insertions(+), 24 deletions(-) (limited to 'src/persist.rs') diff --git a/src/flags.rs b/src/flags.rs index 9432b8f..c93f3ef 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -1,3 +1,9 @@ + +const HELP_FLAG: &str = "help h"; +const VERSION_FLAG: &str = "version v"; +const DONT_PERSIST: &str = "d"; + + pub struct Flags { pub help: bool, pub version: bool, @@ -5,6 +11,13 @@ pub struct Flags { pub arg_count: usize } + +/// 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, @@ -34,14 +47,20 @@ pub fn parse(args: &[String]) -> Option { Some(flags) } + +/// Checks if a given string is a given argument 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"; +/// 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; @@ -56,6 +75,13 @@ fn set_flag(arg: &str, flags: &mut Flags) -> bool { 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 { diff --git a/src/main.rs b/src/main.rs index 51ca0f2..c6e8201 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,13 @@ use std::env; use std::process::ExitCode; use pwd::Passwd; -use nix::unistd; +use nix::unistd::{self, Uid, Group}; + + +mod persist; +mod flags; +mod secure; -extern crate time; const ERROR_ARGS: u8 = 1; const ERROR_CONFIG: u8 = 2; @@ -12,35 +16,47 @@ const ERROR_NOT_AUTHORIZED: u8 = 4; const ERROR_AUTH_FAILED: u8 = 5; const ERROR_RUN_ROOT: u8 = 6; -mod persist; -mod flags; -mod secure; 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(); 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"); return ExitCode::SUCCESS; } + + // If the help arg flag is set, print the crab help message if flags.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; } + + // Load the command config from /etc let config = match config("/etc/crab.conf") { Some(data) => data, None => return ExitCode::from(ERROR_CONFIG) }; + + // get the current user login let user = match Passwd::current_user() { Some(data) => data, None => { @@ -48,6 +64,8 @@ fn main() -> ExitCode { return ExitCode::from(ERROR_NO_USER); } }; + + // check if the user is authorized let persist = match allowed(&config, &user.name) { Some(data) => data && !flags.dont_persist, None => { @@ -56,23 +74,31 @@ fn main() -> ExitCode { } }; + + // authenticate the user if !validate(&user.name, persist) { eprintln!("Authentication failed."); return ExitCode::from(ERROR_AUTH_FAILED); } + // set the uid and gid of the process to root to run the command as root 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); }; + + // 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 } + +/// Prints the help message to the standard output fn help() { let help = "Usage: @@ -84,10 +110,22 @@ Options: println!("{}", help); } + struct Config { - users: Vec<(String, bool)> + identitys: Vec<(String, bool)>, } + +/// Validates the authorized user, and authenticates them 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 +/// * `user` - The login name of the linux user +/// * `persist` - If the user's login should persist +/// #### Returns +/// * `true` - If the user authenticated sucessfully, or the user is persisted +/// * `false` - If the user failed to authenticate fn validate(user: &str, persist: bool) -> bool { if persist && persist::get_persist(user) { return true; @@ -110,15 +148,68 @@ fn validate(user: &str, persist: bool) -> bool { return true; } + +/// Returns if the user is authorized given a sepcific config and user name. +/// #### Arguments +/// * `config` - A config struct contaning the authorization settings for crab +/// * `user` - The name of the user to check is authorized +/// #### Returns +/// * `None` - If the user is not authorized +/// * `Some(bool)` - If the user is authorized, returning the boolean if the +/// user is set to persist fn allowed(config: &Config, user: &str) -> Option { - for (name, persist) in &config.users { - if name == user { + // get user groups + let groups = get_groups(Uid::current()); + // check each config identiy + for (identity, persist) in &config.identitys { + // if it starts with a :, its a group + if identity.starts_with(":") { + let group = &identity[1..]; + if groups.contains(&group.to_string()) { + return Some(persist.clone()); + }; + // else its a user + } else if identity == user { return Some(persist.clone()); } } None } + +/// Returns a vector of group names of the groups a specific linux user is in +/// #### Arguments +/// * 'uid' - The user id od the linux user to get groups from +/// #### 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(uid: Uid) -> Vec { + let no_change = Uid::from_raw(u32::MAX); + if unistd::setresuid(no_change, uid, no_change).is_err() { + return vec![] + }; + let groups = match nix::unistd::getgroups() { + Ok(data) => data, + Err(_) => return vec![] + }; + let names = groups.iter() + .map(|gid| Group::from_gid(*gid)) + .flatten().flatten() + .map(|group| group.name) + .collect(); + names +} + + +/// 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(Config)` - If the config was sucessfully parsed. fn config(path: &str) -> Option { let file = match std::fs::read_to_string(path) { Err(e) => { @@ -128,7 +219,7 @@ fn config(path: &str) -> Option { Ok(data) => data }; - let mut users = vec![]; + let mut identitys = vec![]; for (line_num, line) in file.split("\n").enumerate() { let args: Vec<&str> = line.split(" ").collect(); if line.trim() == "" { @@ -138,7 +229,7 @@ fn config(path: &str) -> Option { eprintln!("Error in config at line {}: Not enough arguments", line_num); continue; } - let user: String = args[0].to_string(); + let identity: String = args[0].to_string(); let persist: bool = match args[1].parse() { Err(e) => { eprintln!("Error in config at line {}: {}", line_num, e); @@ -146,7 +237,7 @@ fn config(path: &str) -> Option { }, Ok(data) => data }; - users.push((user, persist)); + identitys.push((identity, persist)); } - Some(Config{users}) + Some(Config{identitys}) } diff --git a/src/persist.rs b/src/persist.rs index 667b12a..fe4ce69 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -1,13 +1,22 @@ 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() { + let json = match get_persist_config() { Some(data) => data, None => return false }; @@ -18,17 +27,21 @@ pub fn get_persist(user: &str) -> bool { return now() - timestamp < PERSIST_TIME && timestamp - 1 < now(); } + +/// Updates 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() { + let mut json = match get_persist_config() { Some(data) => data, None => serde_json::from_str("{}").unwrap() }; json[user] = Value::from(now()); - let id = match get_terminal_process() { + let session = match get_current_session() { Some(data) => data, None => return }; - match secure::write_file(PERSIST_PATH, &format!("{}", id), &json.to_string()) { + match secure::write_file(PERSIST_PATH, &format!("{}", session), &json.to_string()) { Ok(_) => {}, Err(_) => { eprintln!("crab: An Internal Has Error") @@ -36,7 +49,12 @@ pub fn set_persist(user: &str) { }; } -fn get_terminal_process() -> Option { + +/// 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 @@ -48,12 +66,17 @@ fn get_terminal_process() -> Option { Some(pid_stat.session) } -fn get_terminal_config() -> Option { - let id = match get_terminal_process() { + +/// 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!("{}", id)) { + let data = match secure::read_file(PERSIST_PATH, &format!("{}", session)) { Some(data) => data, None => "{}".to_string() }; @@ -64,6 +87,8 @@ fn get_terminal_config() -> Option { 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(); } diff --git a/src/secure.rs b/src/secure.rs index 5fb0cc8..39339a0 100644 --- a/src/secure.rs +++ b/src/secure.rs @@ -1,6 +1,14 @@ use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io}; use nix::unistd; + +/// 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)?; @@ -11,6 +19,16 @@ pub fn write_file(dir: &str, file: &str, data: &str) -> Result<(), io::Error> { 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 !check_file_permissions(0, 0, 0o100600, &path) { @@ -22,6 +40,15 @@ pub fn read_file(dir: &str, file: &str) -> Option { }; } + +/// 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(std::path::Path::new(path), Some(unistd::Uid::from(uid)), Some(unistd::Gid::from(gid)))?; let metadata = fs::metadata(path)?; @@ -31,6 +58,15 @@ fn set_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> Result<(), 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, @@ -40,6 +76,8 @@ fn check_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> bool { 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 -- cgit v1.2.3-freya From b457c08923f1ef8d88005cf9781d1b4d5dd9552e Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Thu, 10 Nov 2022 16:22:29 -0500 Subject: move root priv fn, slight refactor --- src/main.rs | 12 +++--------- src/persist.rs | 10 ++++++---- src/secure.rs | 25 +++++++++++++++++++------ 3 files changed, 28 insertions(+), 19 deletions(-) (limited to 'src/persist.rs') diff --git a/src/main.rs b/src/main.rs index c6e8201..850d43b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,6 @@ 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; fn main() -> ExitCode { @@ -74,19 +73,12 @@ fn main() -> ExitCode { } }; - // authenticate the user if !validate(&user.name, persist) { eprintln!("Authentication failed."); return ExitCode::from(ERROR_AUTH_FAILED); } - // set the uid and gid of the process to root to run the command as root - 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); - }; - // execute the passed command let start = 1 + flags.arg_count; let err = exec::execvp(&args[start], &args[start..]); @@ -128,6 +120,7 @@ struct Config { /// * `false` - If the user failed to authenticate fn validate(user: &str, persist: bool) -> bool { if persist && persist::get_persist(user) { + secure::elevate_privilages(0, 0); return true; } let input = match rpassword::prompt_password(format!("crab ({}) password: ", user)) { @@ -142,6 +135,7 @@ fn validate(user: &str, persist: bool) -> bool { if !auth.authenticate().is_ok() || !auth.open_session().is_ok() { return false; } + secure::elevate_privilages(0, 0); if persist { persist::set_persist(user); } @@ -222,7 +216,7 @@ fn config(path: &str) -> Option { let mut identitys = vec![]; for (line_num, line) in file.split("\n").enumerate() { let args: Vec<&str> = line.split(" ").collect(); - if line.trim() == "" { + if line.starts_with("#") || line.trim() == "" { continue; } if args.len() < 2 { diff --git a/src/persist.rs b/src/persist.rs index fe4ce69..0e0d1fd 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -34,7 +34,7 @@ pub fn get_persist(user: &str) -> bool { pub fn set_persist(user: &str) { let mut json = match get_persist_config() { Some(data) => data, - None => serde_json::from_str("{}").unwrap() + None => return }; json[user] = Value::from(now()); let session = match get_current_session() { @@ -43,8 +43,8 @@ pub fn set_persist(user: &str) { }; match secure::write_file(PERSIST_PATH, &format!("{}", session), &json.to_string()) { Ok(_) => {}, - Err(_) => { - eprintln!("crab: An Internal Has Error") + Err(e) => { + eprintln!("crab: An Internal Has Error: {}", e); } }; } @@ -78,7 +78,9 @@ fn get_persist_config() -> Option { }; let data = match secure::read_file(PERSIST_PATH, &format!("{}", session)) { Some(data) => data, - None => "{}".to_string() + None => { + "{}".to_string() + } }; let json: Value = match serde_json::from_str(&data) { Ok(data) => data, diff --git a/src/secure.rs b/src/secure.rs index 39339a0..f463048 100644 --- a/src/secure.rs +++ b/src/secure.rs @@ -1,5 +1,5 @@ -use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io}; -use nix::unistd; +use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io::{self, ErrorKind}}; +use nix::unistd::{self, Uid, Gid}; /// Writes a file securly to a specified path with given data @@ -11,10 +11,10 @@ use nix::unistd; /// 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)?; + set_file_permissions(0, 0, 0o600, dir)?; let path = path(dir, file); fs::write(&path, "")?; - set_file_permissions(0, 0, 0o100600, &path)?; + set_file_permissions(0, 0, 0o600, &path)?; fs::write(&path, data)?; Ok(()) } @@ -41,6 +41,17 @@ pub fn read_file(dir: &str, file: &str) -> Option { } +pub fn elevate_privilages(uid: u32, gid: u32) -> bool { + if unistd::setuid(Uid::from(uid)).is_err() { + return false; + } + if unistd::setgid(Gid::from(gid)).is_err() { + return false; + } + true +} + + /// Sets the permission for a secure file /// #### Arguments /// * `uid` - The user to own the file @@ -70,7 +81,9 @@ fn set_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> Result<(), fn check_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> bool { let metadata = match fs::metadata(path) { Ok(data) => data, - Err(_) => return false + Err(e) => { + return e.kind() == ErrorKind::NotFound; + } }; let perms = metadata.permissions(); return perms.mode() == mode && metadata.st_uid() == uid && metadata.st_gid() == gid; @@ -79,5 +92,5 @@ fn check_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> bool { /// Get the path of a file given a directory and file name fn path(dir: &str, file: &str) -> String { - return format!("{}/{}", dir, file); + return format!("{}/{}.persist", dir, file); } \ No newline at end of file -- cgit v1.2.3-freya From 9bb5154ae8c09616b1a9a1caa29325f8dd3b6ebd Mon Sep 17 00:00:00 2001 From: tylermurphy534 Date: Thu, 10 Nov 2022 19:19:20 -0500 Subject: finalize adding groups --- src/auth.rs | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 144 ++------------------------------------------------------- src/persist.rs | 4 +- src/secure.rs | 5 +- 4 files changed, 134 insertions(+), 145 deletions(-) create mode 100644 src/auth.rs (limited to 'src/persist.rs') diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..c973921 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,126 @@ +use nix::unistd::Group; +use crate::{persist, secure}; + + +pub struct Config { + pub identitys: Vec<(String, bool)>, +} + + +/// 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(Config)` - If the config was sucessfully parsed. +pub fn load_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 identitys = vec![]; + for (line_num, line) in file.split("\n").enumerate() { + let args: Vec<&str> = line.split(" ").collect(); + if line.starts_with("#") || line.trim() == "" { + continue; + } + if args.len() < 2 { + eprintln!("Error in config at line {}: Not enough arguments", line_num); + continue; + } + let identity: 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 + }; + identitys.push((identity, persist)); + } + Some(Config{identitys}) +} + + +/// 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 nix::unistd::getgroups() { + Ok(data) => data, + Err(_) => return vec![] + }; + let names = groups.iter() + .map(|gid| Group::from_gid(*gid)) + .flatten().flatten() + .map(|group| group.name) + .collect(); + names +} + + +/// Returns if the user is authorized given a sepcific config and user name. +/// #### Arguments +/// * `config` - A config struct contaning the authorization settings for crab +/// * `user` - The name of the user to check is authorized +/// #### Returns +/// * `None` - If the user is not authorized +/// * `Some(bool)` - If the user is authorized, returning the boolean if the +/// user is set to persist +pub fn authorize(config: &Config, user: &str) -> Option { + let groups = get_groups(); + for (identity, persist) in &config.identitys { + if identity.starts_with(":") { + let group = &identity[1..]; + if groups.contains(&group.to_string()) { + return Some(persist.clone()); + }; + } else if identity == user { + return Some(persist.clone()); + } + } + 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 +/// * `user` - The login name of the linux user +/// * `persist` - If the user's login should persist +/// #### Returns +/// * `true` - If the user authenticated sucessfully, or the user is persisted +/// * `false` - If the user failed to authenticate +pub fn authenticate(user: &str, persist: bool) -> bool { + if persist && persist::get_persist(user) { + secure::elevate_privilages(0, 0); + 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); + } + secure::elevate_privilages(0, 0); + return true; +} diff --git a/src/main.rs b/src/main.rs index 850d43b..ed89e76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ use std::env; use std::process::ExitCode; use pwd::Passwd; -use nix::unistd::{self, Uid, Group}; -mod persist; +mod auth; mod flags; +mod persist; mod secure; @@ -50,7 +50,7 @@ fn main() -> ExitCode { } // Load the command config from /etc - let config = match config("/etc/crab.conf") { + let config = match auth::load_config("/etc/crab.conf") { Some(data) => data, None => return ExitCode::from(ERROR_CONFIG) }; @@ -65,7 +65,7 @@ fn main() -> ExitCode { }; // check if the user is authorized - let persist = match allowed(&config, &user.name) { + let persist = match auth::authorize(&config, &user.name) { Some(data) => data && !flags.dont_persist, None => { eprintln!("Operation Not Permitted."); @@ -74,7 +74,7 @@ fn main() -> ExitCode { }; // authenticate the user - if !validate(&user.name, persist) { + if !auth::authenticate(&user.name, persist) { eprintln!("Authentication failed."); return ExitCode::from(ERROR_AUTH_FAILED); } @@ -101,137 +101,3 @@ Options: -d If your user is set to persist, dont save persistance"; println!("{}", help); } - - -struct Config { - identitys: Vec<(String, bool)>, -} - - -/// Validates the authorized user, and authenticates them 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 -/// * `user` - The login name of the linux user -/// * `persist` - If the user's login should persist -/// #### Returns -/// * `true` - If the user authenticated sucessfully, or the user is persisted -/// * `false` - If the user failed to authenticate -fn validate(user: &str, persist: bool) -> bool { - if persist && persist::get_persist(user) { - secure::elevate_privilages(0, 0); - 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; - } - secure::elevate_privilages(0, 0); - if persist { - persist::set_persist(user); - } - return true; -} - - -/// Returns if the user is authorized given a sepcific config and user name. -/// #### Arguments -/// * `config` - A config struct contaning the authorization settings for crab -/// * `user` - The name of the user to check is authorized -/// #### Returns -/// * `None` - If the user is not authorized -/// * `Some(bool)` - If the user is authorized, returning the boolean if the -/// user is set to persist -fn allowed(config: &Config, user: &str) -> Option { - // get user groups - let groups = get_groups(Uid::current()); - // check each config identiy - for (identity, persist) in &config.identitys { - // if it starts with a :, its a group - if identity.starts_with(":") { - let group = &identity[1..]; - if groups.contains(&group.to_string()) { - return Some(persist.clone()); - }; - // else its a user - } else if identity == user { - return Some(persist.clone()); - } - } - None -} - - -/// Returns a vector of group names of the groups a specific linux user is in -/// #### Arguments -/// * 'uid' - The user id od the linux user to get groups from -/// #### 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(uid: Uid) -> Vec { - let no_change = Uid::from_raw(u32::MAX); - if unistd::setresuid(no_change, uid, no_change).is_err() { - return vec![] - }; - let groups = match nix::unistd::getgroups() { - Ok(data) => data, - Err(_) => return vec![] - }; - let names = groups.iter() - .map(|gid| Group::from_gid(*gid)) - .flatten().flatten() - .map(|group| group.name) - .collect(); - names -} - - -/// 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(Config)` - If the config was sucessfully parsed. -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 identitys = vec![]; - for (line_num, line) in file.split("\n").enumerate() { - let args: Vec<&str> = line.split(" ").collect(); - if line.starts_with("#") || line.trim() == "" { - continue; - } - if args.len() < 2 { - eprintln!("Error in config at line {}: Not enough arguments", line_num); - continue; - } - let identity: 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 - }; - identitys.push((identity, persist)); - } - Some(Config{identitys}) -} diff --git a/src/persist.rs b/src/persist.rs index 0e0d1fd..1cddb7f 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -78,9 +78,7 @@ fn get_persist_config() -> Option { }; let data = match secure::read_file(PERSIST_PATH, &format!("{}", session)) { Some(data) => data, - None => { - "{}".to_string() - } + None => "{}".to_string() }; let json: Value = match serde_json::from_str(&data) { Ok(data) => data, diff --git a/src/secure.rs b/src/secure.rs index f463048..352e02b 100644 --- a/src/secure.rs +++ b/src/secure.rs @@ -11,11 +11,10 @@ use nix::unistd::{self, Uid, Gid}; /// 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, 0o600, dir)?; + set_file_permissions(0, 0, 0o100600, dir)?; let path = path(dir, file); - fs::write(&path, "")?; - set_file_permissions(0, 0, 0o600, &path)?; fs::write(&path, data)?; + set_file_permissions(0, 0, 0o100600, &path)?; Ok(()) } -- cgit v1.2.3-freya From 58208a126858d14e4d4bf4707e298919d234bc22 Mon Sep 17 00:00:00 2001 From: tylermurphy534 Date: Fri, 11 Nov 2022 01:25:10 -0500 Subject: update config format --- Cargo.lock | 68 +----------- Cargo.toml | 3 +- conf | 6 +- deployments/aur/.SRCINFO | 2 +- deployments/aur/PKGBUILD | 8 +- install.sh | 8 +- src/auth.rs | 279 ++++++++++++++++++++++++++++++++--------------- src/flags.rs | 88 +++++++-------- src/main.rs | 24 ++-- src/persist.rs | 94 ++++++++-------- src/secure.rs | 78 +++++++------ uninstall.sh | 9 +- 12 files changed, 349 insertions(+), 318 deletions(-) (limited to 'src/persist.rs') 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 index bebb505..0da97e5 100644 --- a/conf +++ b/conf @@ -1 +1,5 @@ -root true \ No newline at end of file +permit nopass linus as root +deny :docker +#deny jane +permit persist :wheel +permit jane as doe \ No newline at end of file diff --git a/deployments/aur/.SRCINFO b/deployments/aur/.SRCINFO index 66c6ce5..c145393 100644 --- a/deployments/aur/.SRCINFO +++ b/deployments/aur/.SRCINFO @@ -1,6 +1,6 @@ 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 arch = x86_64 diff --git a/deployments/aur/PKGBUILD b/deployments/aur/PKGBUILD index b49ce75..6196e48 100644 --- a/deployments/aur/PKGBUILD +++ b/deployments/aur/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Tyler Murphy pkgname=crab -pkgver=0.0.5 +pkgver=0.0.6 pkgrel=2 pkgdesc="A rusty permission authentication system" arch=('x86_64' 'i686') @@ -18,7 +18,7 @@ build() { 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 + install -D --mode=4755 --owner=root --group=root ./target/release/crab ${pkgdir}/usr/bin/crab + install -D --mode=600 --owner=root --group=root pam ${pkgdir}/etc/pam.d/crab + install -D --mode=600 --owner=root --group=root conf ${pkgdir}/usr/share/crab/crab.conf } diff --git a/install.sh b/install.sh index 716d9b1..6c7b019 100755 --- a/install.sh +++ b/install.sh @@ -8,10 +8,14 @@ fi # Copy executable to bin cp ./target/release/crab /usr/bin/crab chown root:root /bin/crab -chmod 6755 /bin/crab +chmod 4755 /bin/crab # Set up config files cp pam /etc/pam.d/crab +chmod 600 /etc/pam.d/crab + mkdir /usr/share/crab +chmod 600 /usr/share/crab + cp conf /usr/share/crab/crab.conf -chmod 660 /usr/share/crab/crab.conf +chmod 600 /usr/share/crab/crab.conf diff --git a/src/auth.rs b/src/auth.rs index c973921..82a09cb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,9 +1,15 @@ -use nix::unistd::Group; +use nix::unistd::{User, Group, Uid, Gid}; use crate::{persist, secure}; +#[derive(Debug)] pub struct Config { - pub identitys: Vec<(String, bool)>, + pub permit: bool, + pub persist: bool, + pub nopass: bool, + pub user_uid: Option, + pub user_gid: Option, + pub privlaged_uid: Uid, } @@ -14,37 +20,132 @@ pub struct Config { /// * `path` - The path to the config file /// #### Returns /// * `None` - If the config failed to load -/// * `Some(Config)` - If the config was sucessfully parsed. -pub fn load_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 identitys = vec![]; - for (line_num, line) in file.split("\n").enumerate() { - let args: Vec<&str> = line.split(" ").collect(); - if line.starts_with("#") || line.trim() == "" { - continue; - } - if args.len() < 2 { - eprintln!("Error in config at line {}: Not enough arguments", line_num); - continue; - } - let identity: 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 - }; - identitys.push((identity, persist)); - } - Some(Config{identitys}) +/// * `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, as_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) + }, + None => (args[len-1].to_string(), "root".to_string(), len-1) + }; + + let persist = args[1..as_index].contains(&"persist"); + + let nopass = args[1..as_index].contains(&"nopass"); + + + 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) +} + + +fn config_error(line_num: usize, message: &str) { + eprintln!("Error in config at line {}: {}", line_num, message); +} + + +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 + } +} + + +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 + } +} + + +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 + } } @@ -53,41 +154,44 @@ pub fn load_config(path: &str) -> Option { /// 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 nix::unistd::getgroups() { - Ok(data) => data, - Err(_) => return vec![] - }; - let names = groups.iter() - .map(|gid| Group::from_gid(*gid)) - .flatten().flatten() - .map(|group| group.name) - .collect(); - names +fn get_groups() -> Vec { + let groups = match nix::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 -/// * `user` - The name of the user to check is authorized +/// * `uid` - The uid to check is authorized /// #### Returns -/// * `None` - If the user is not authorized -/// * `Some(bool)` - If the user is authorized, returning the boolean if the -/// user is set to persist -pub fn authorize(config: &Config, user: &str) -> Option { - let groups = get_groups(); - for (identity, persist) in &config.identitys { - if identity.starts_with(":") { - let group = &identity[1..]; - if groups.contains(&group.to_string()) { - return Some(persist.clone()); - }; - } else if identity == user { - return Some(persist.clone()); - } - } - None +/// * `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 } @@ -96,31 +200,36 @@ pub fn authorize(config: &Config, user: &str) -> Option { /// read the persist file, and then skip authentication if /// the is still persisted. /// #### Arguments -/// * `user` - The login name of the linux user -/// * `persist` - If the user's login should persist +/// * `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, or the user is persisted +/// * `true` - If the user authenticated sucessfully /// * `false` - If the user failed to authenticate -pub fn authenticate(user: &str, persist: bool) -> bool { - if persist && persist::get_persist(user) { - secure::elevate_privilages(0, 0); - 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, +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) ) { + secure::elevate_privilages(config.privlaged_uid); + 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(user.to_owned(), input); - if !auth.authenticate().is_ok() || !auth.open_session().is_ok() { - return false; - } - if persist { - persist::set_persist(user); - } - secure::elevate_privilages(0, 0); - return true; + }; + auth.get_handler().set_credentials(&name, input); + if !auth.authenticate().is_ok() || !auth.open_session().is_ok() { + return false; + } + if config.persist { + persist::set_persist(&name); + } + secure::elevate_privilages(config.privlaged_uid); + return true; } diff --git a/src/flags.rs b/src/flags.rs index c93f3ef..a945483 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -5,10 +5,10 @@ const DONT_PERSIST: &str = "d"; pub struct Flags { - pub help: bool, - pub version: bool, - pub dont_persist: bool, - pub arg_count: usize + pub help: bool, + pub version: bool, + pub dont_persist: bool, + pub arg_count: usize } @@ -19,32 +19,32 @@ pub struct Flags { /// * `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 + 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) + Some(flags) } @@ -62,17 +62,17 @@ fn is_arg(arg: &str) -> bool { /// * `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 + 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 } @@ -83,10 +83,10 @@ fn set_flag(arg: &str, flags: &mut Flags) -> bool { /// * `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 + for check_arg in check.split(" ") { + if check_arg == arg { + return true + } } - } - return false + return false } diff --git a/src/main.rs b/src/main.rs index ed89e76..cca85de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ use std::env; use std::process::ExitCode; -use pwd::Passwd; mod auth; @@ -11,9 +10,8 @@ mod secure; 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_NOT_AUTHORIZED: u8 = 3; +const ERROR_AUTH_FAILED: u8 = 4; fn main() -> ExitCode { @@ -33,7 +31,7 @@ fn main() -> ExitCode { // 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; } @@ -50,23 +48,15 @@ fn main() -> ExitCode { } // Load the command config from /etc - let config = match auth::load_config("/etc/crab.conf") { + let configs = match auth::load_config_file("/etc/crab.conf") { Some(data) => data, None => return ExitCode::from(ERROR_CONFIG) }; - // get the current user login - let user = match Passwd::current_user() { - Some(data) => data, - None => { - eprintln!("You dont exist."); - return ExitCode::from(ERROR_NO_USER); - } - }; // check if the user is authorized - let persist = match auth::authorize(&config, &user.name) { - Some(data) => data && !flags.dont_persist, + let auth = match auth::authorize(&configs, nix::unistd::getuid()) { + Some(data) => data, None => { eprintln!("Operation Not Permitted."); return ExitCode::from(ERROR_NOT_AUTHORIZED); @@ -74,7 +64,7 @@ fn main() -> ExitCode { }; // authenticate the user - if !auth::authenticate(&user.name, persist) { + if !auth::authenticate(&configs[auth], flags.dont_persist, nix::unistd::getuid()) { eprintln!("Authentication failed."); return ExitCode::from(ERROR_AUTH_FAILED); } diff --git a/src/persist.rs b/src/persist.rs index 1cddb7f..762d8b1 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -16,15 +16,15 @@ const PERSIST_PATH: &str = "/var/run/crab"; /// * `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_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(); + 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(); } @@ -32,21 +32,21 @@ pub fn get_persist(user: &str) -> bool { /// #### Arguments /// * `user` - The user to set persisted pub fn set_persist(user: &str) { - 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(e) => { - eprintln!("crab: An Internal Has 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(e) => { + eprintln!("crab: An Internal Has Error: {}", e); + } + }; } @@ -55,15 +55,15 @@ pub fn set_persist(user: &str) { /// * `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) + 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) } @@ -72,23 +72,23 @@ fn get_current_session() -> Option { /// * `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) + 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 13d67b0..018bc4f 100644 --- a/src/secure.rs +++ b/src/secure.rs @@ -1,5 +1,5 @@ use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io::{self, ErrorKind}}; -use nix::unistd::{self, Uid, Gid}; +use nix::unistd::{self, Uid}; /// Writes a file securly to a specified path with given data @@ -10,12 +10,12 @@ use nix::unistd::{self, Uid, Gid}; /// #### 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(()) + 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(()) } @@ -29,31 +29,27 @@ pub fn write_file(dir: &str, file: &str, data: &str) -> Result<(), io::Error> { /// * `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 !check_file_permissions(0, 0, 0o100600, &path) { - return None; - } - match fs::read_to_string(&path) { - Ok(data) => return Some(data), - Err(_) => return None - }; + let path = path(dir,file); + if !check_file_permissions(0, 0, 0o100600, &path) { + return None; + } + match fs::read_to_string(&path) { + Ok(data) => return Some(data), + Err(_) => return None + }; } -/// Ekevate tge oruvukages if the current process +/// Elecate the privlages of the current process /// #### Arguments /// * `uid` - The uid to set the process to -/// * `gid` - The gid to set the process to /// #### Returns -/// If the process failes to elevate, it returns false -pub fn elevate_privilages(uid: u32, gid: u32) -> bool { - if unistd::setuid(Uid::from(uid)).is_err() { - return false; - } - if unistd::setgid(Gid::from(gid)).is_err() { - return false; - } - true +/// If the process fails to elevate, it returns false +pub fn elevate_privilages(uid: Uid) -> bool { + if unistd::setuid(uid).is_err() { + return false; + } + true } @@ -66,12 +62,12 @@ pub fn elevate_privilages(uid: u32, gid: u32) -> bool { /// #### 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(std::path::Path::new(path), Some(unistd::Uid::from(uid)), Some(unistd::Gid::from(gid)))?; - let metadata = fs::metadata(path)?; - let mut perms = metadata.permissions(); - perms.set_mode(mode); - fs::set_permissions(path, perms)?; - Ok(()) + unistd::chown(std::path::Path::new(path), Some(unistd::Uid::from(uid)), Some(unistd::Gid::from(gid)))?; + let metadata = fs::metadata(path)?; + let mut perms = metadata.permissions(); + perms.set_mode(mode); + fs::set_permissions(path, perms)?; + Ok(()) } @@ -84,18 +80,18 @@ fn set_file_permissions(uid: u32, gid: u32, mode: u32, path: &str) -> Result<(), /// #### 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; + 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!("{}/{}.persist", dir, file); + return format!("{}/{}.persist", dir, file); } diff --git a/uninstall.sh b/uninstall.sh index f6cba7f..cf7ade8 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -6,11 +6,6 @@ if [[ $(/usr/bin/id -u) -ne 0 ]]; then fi # Delete crab files +rm /etc/pam.d/crab 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 +rm -fr /usr/share/crab -- cgit v1.2.3-freya From de24d5499ac83f426228b28b90bed8f26e68fb3b Mon Sep 17 00:00:00 2001 From: tylermurphy534 Date: Fri, 11 Nov 2022 09:56:48 -0500 Subject: doc string, refactor, config token error --- readme.md | 14 +++++++++++--- src/auth.rs | 51 ++++++++++++++++++++++++++++++++++++++++----------- src/main.rs | 15 +++++++++++---- src/persist.rs | 28 +++++++++++++++++++++++++--- src/secure.rs | 19 +++---------------- 5 files changed, 90 insertions(+), 37 deletions(-) (limited to 'src/persist.rs') diff --git a/readme.md b/readme.md index 971c469..7d6566d 100644 --- a/readme.md +++ b/readme.md @@ -12,11 +12,19 @@ Run `uninstall.sh` as root to uninstall crab. If you are on an arch based distro, crab is avaliable on the [AUR](https://aur.archlinux.org/packages/crab) as `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. +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 +permit :wheel persist +#deny stallman +permit nvidia as fu ``` The default configuration file is stored in `/usr/share/crab/crab.conf` and must be coppied to `/etc/crab.conf`. diff --git a/src/auth.rs b/src/auth.rs index 82a09cb..99a8216 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,8 +1,7 @@ -use nix::unistd::{User, Group, Uid, Gid}; -use crate::{persist, secure}; +use nix::unistd::{User, Group, Uid, Gid, self}; +use crate::persist; -#[derive(Debug)] pub struct Config { pub permit: bool, pub persist: bool, @@ -55,20 +54,31 @@ pub fn load_config_file(path: &str) -> Option> { } }; - let (user_name, privlaged_name, as_index) = match args.iter().position(|&a| a == "as") { + 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) + (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..as_index].contains(&"persist"); + let persist = args[1..name_index].contains(&"persist"); - let nopass = args[1..as_index].contains(&"nopass"); + 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) = @@ -111,11 +121,18 @@ pub fn load_config_file(path: &str) -> Option> { } +/// 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 { @@ -127,6 +144,12 @@ fn get_uid_from_name(name: &str) -> Option { } +/// 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 { @@ -138,6 +161,12 @@ fn get_name_from_uid(uid: Uid) -> Option { } +/// 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 { @@ -155,7 +184,7 @@ fn get_gid_from_name(name: &str) -> Option { /// either the function coudn't retrieve the users groups, or the user is not in /// any groups. fn get_groups() -> Vec { - let groups = match nix::unistd::getgroups() { + let groups = match unistd::getgroups() { Ok(data) => data, Err(_) => return vec![] }; @@ -212,7 +241,6 @@ pub fn authenticate(config: &Config, force_pass: bool, uid: Uid) -> bool { None => return false }; if config.nopass || ( !force_pass && config.persist && persist::get_persist(&name) ) { - secure::elevate_privilages(config.privlaged_uid); return true; } let input = match rpassword::prompt_password(format!("crab ({}) password: ", &name)) { @@ -227,9 +255,10 @@ pub fn authenticate(config: &Config, force_pass: bool, uid: Uid) -> bool { if !auth.authenticate().is_ok() || !auth.open_session().is_ok() { return false; } - if config.persist { + if !force_pass && config.persist { persist::set_persist(&name); + } else if force_pass { + persist::remove_persist(&name); } - secure::elevate_privilages(config.privlaged_uid); return true; } diff --git a/src/main.rs b/src/main.rs index cca85de..56df611 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,11 @@ 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_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 { @@ -69,6 +70,12 @@ fn main() -> ExitCode { return ExitCode::from(ERROR_AUTH_FAILED); } + // 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..]); diff --git a/src/persist.rs b/src/persist.rs index 762d8b1..2bca386 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -28,7 +28,7 @@ pub fn get_persist(user: &str) -> bool { } -/// Updates the current sessions persist file +/// Updates a user in the current sessions persist file /// #### Arguments /// * `user` - The user to set persisted pub fn set_persist(user: &str) { @@ -43,8 +43,30 @@ pub fn set_persist(user: &str) { }; match secure::write_file(PERSIST_PATH, &format!("{}", session), &json.to_string()) { Ok(_) => {}, - Err(e) => { - eprintln!("crab: An Internal Has Error: {}", e); + Err(_) => { + eprintln!("crab: An internal error has occured"); + } + }; +} + + +/// 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"); } }; } diff --git a/src/secure.rs b/src/secure.rs index 018bc4f..7c85ece 100644 --- a/src/secure.rs +++ b/src/secure.rs @@ -1,5 +1,5 @@ -use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io::{self, ErrorKind}}; -use nix::unistd::{self, Uid}; +use std::{os::{unix::prelude::PermissionsExt, linux::fs::MetadataExt}, fs, io::{self, ErrorKind}, path::Path}; +use nix::unistd::{self, Uid, Gid}; /// Writes a file securly to a specified path with given data @@ -40,19 +40,6 @@ pub fn read_file(dir: &str, file: &str) -> Option { } -/// Elecate the privlages of the current process -/// #### Arguments -/// * `uid` - The uid to set the process to -/// #### Returns -/// If the process fails to elevate, it returns false -pub fn elevate_privilages(uid: Uid) -> bool { - if unistd::setuid(uid).is_err() { - return false; - } - true -} - - /// Sets the permission for a secure file /// #### Arguments /// * `uid` - The user to own the file @@ -62,7 +49,7 @@ pub fn elevate_privilages(uid: Uid) -> bool { /// #### 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(std::path::Path::new(path), Some(unistd::Uid::from(uid)), Some(unistd::Gid::from(gid)))?; + 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); -- cgit v1.2.3-freya