Skip to content

Commit

Permalink
refactor: comments & documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
wandapeter committed Dec 22, 2023
1 parent 3697b44 commit c764566
Show file tree
Hide file tree
Showing 6 changed files with 387 additions and 49 deletions.
105 changes: 99 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
//! # RampDelay PAM Module
//!
//! The RampDelay PAM (Pluggable Authentication Modules) module provides an account lockout mechanism
//! based on the number of authentication failures. It calculates a dynamic delay for subsequent
//! authentication attempts, increasing the delay with each failure to mitigate brute force attacks.
//!
//! ## Usage
//!
//! To use the RampDelay PAM module, integrate it with the PAM system by configuring the `/etc/pam.d/`
//! configuration files for the desired PAM-aware services. This module is designed for the
//! `sm_authenticate` and `acct_mgmt` hooks.
//!
//! ## Configuration
//!
//! The behavior of the RampDelay module is configurable through an INI file located at
//! `/etc/security/rampdelay.conf` by default. The configuration file can be customized with settings
//! such as the tally directory, free tries threshold, base delay, and multiplier.
//!
//! ```ini
//! [Settings]
//! tally_dir = /var/run/rampdelay
//! free_tries = 6
//! base_delay_seconds = 30
//! ramp_multiplier = 1.5
//! ```
//!
//! - `tally_dir`: Directory where tally information is stored.
//! - `free_tries`: Number of allowed free authentication attempts before applying delays.
//! - `base_delay_seconds`: Base delay applied to each authentication failure.
//! - `ramp_multiplier`: Multiplier for the delay calculation based on the number of failures.
//!
//! ## License
//!
//! pam-authramp
//! Copyright (C) 2023 github.com/34N0
//!
//! This program is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! This program is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with this program. If not, see <http://www.gnu.org/licenses/>.
mod settings;
mod tally;

Expand Down Expand Up @@ -29,35 +78,66 @@ pub enum Actions {
AUTHFAIL,
}

/// Initializes the RampDelay module by setting up user information and loading settings.
/// Calls the provided pam_hook function with the initialized variables.
///
/// # Arguments
/// - `pamh`: PamHandle instance for interacting with PAM
/// - `_args`: PAM arguments provided during authentication
/// - `_flags`: PAM flags indicating the context of the PAM operation
/// - `pam_hook`: Function to be called with the initialized variables
///
/// # Returns
/// Result from the pam_hook function or PAM error code if initialization fails
fn init_rampdelay<F, R>(
pamh: &mut PamHandle,
_args: Vec<&CStr>,
_flags: PamFlag,
callback: F,
pam_hook: F,
) -> Result<R, PamResultCode>
where
F: FnOnce(&mut PamHandle, &Settings, &Tally) -> Result<R, PamResultCode>,
{
// Initialize variables
// Try to get PAM user
let user = get_user_by_name(pam_try!(
&pamh.get_user(None),
Err(PamResultCode::PAM_AUTH_ERR)
));

// Read configuration file
let settings = Settings::build(user.clone(), _args, _flags, None)?;

// Get and Set tally
let tally = Tally::open(&settings)?;

callback(pamh, &settings, &tally)
pam_hook(pamh, &settings, &tally)
}

// delay=multi(tries-free)⋅log(tries-free)+base
/// Calculates the delay based on the number of authentication failures and settings.
/// Uses the RampDelay formula: delay = multiplier * (log(fails - free_tries) + base_delay)
///
/// # Arguments
/// - `fails`: Number of authentication failures
/// - `settings`: Settings for the RampDelay module
///
/// # Returns
/// Calculated delay as a floating-point number
fn calc_delay(fails: i32, settings: &Settings) -> f64 {
settings.ramp_multiplier as f64
* (fails as f64 - settings.free_tries as f64)
* ((fails as f64 - settings.free_tries as f64).ln())
+ settings.base_delay_seconds as f64
}

fn fmt_remaining_time(remaining_time: Duration) -> String {
/// Formats a Duration into a human-readable string representation.
/// The format includes hours, minutes, and seconds, excluding zero values.
///
/// # Arguments
/// - `remaining_time`: Duration representing the remaining time
///
/// # Returns
/// Formatted string indicating the remaining time
fn format_remaining_time(remaining_time: Duration) -> String {
let mut formatted_time = String::new();

if remaining_time.num_hours() > 0 {
Expand All @@ -73,6 +153,16 @@ fn fmt_remaining_time(remaining_time: Duration) -> String {
formatted_time
}

/// Handles the account lockout mechanism based on the number of failures and settings.
/// If the account is locked, it sends periodic messages to the user until the account is unlocked.
///
/// # Arguments
/// - `pamh`: PamHandle instance for interacting with PAM
/// - `settings`: Settings for the RampDelay module
/// - `tally`: Tally information containing failure count and timestamps
///
/// # Returns
/// PAM_SUCCESS if the account is successfully unlocked, PAM_AUTH_ERR otherwise
fn bounce_auth(pamh: &mut PamHandle, settings: &Settings, tally: &Tally) -> PamResultCode {
if tally.failures_count > settings.free_tries {
if let Ok(Some(conv)) = pamh.get_item::<Conv>() {
Expand All @@ -90,7 +180,7 @@ fn bounce_auth(pamh: &mut PamHandle, settings: &Settings, tally: &Tally) -> PamR
PAM_ERROR_MSG,
&format!(
"Account locked! Unlocking in {}.",
fmt_remaining_time(remaining_time)
format_remaining_time(remaining_time)
),
);

Expand All @@ -112,14 +202,17 @@ pam::pam_hooks!(PamRampDelay);
impl PamHooks for PamRampDelay {
fn sm_authenticate(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
init_rampdelay(pamh, args, flags, |pamh, settings, tally| {
// match action parameter
match settings.action {
Some(Actions::PREAUTH) => {
// if account is locked then bounce
if tally.failures_count > settings.free_tries {
Err(bounce_auth(pamh, settings, tally))
} else {
Ok(PamResultCode::PAM_SUCCESS)
}
}
// bounce if called with authfail
Some(Actions::AUTHFAIL) => Err(bounce_auth(pamh, settings, tally)),
None | Some(Actions::AUTHSUCC) => Err(PamResultCode::PAM_AUTH_ERR),
}
Expand Down
92 changes: 82 additions & 10 deletions src/settings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
//! # Settings Module
//!
//! The `settings` module is responsible for managing configuration settings related to the
//! rampdelay PAM module. It provides a structure `Settings` and functions to load configuration
//! from an INI file, build settings based on user input, and set default values.
//!
//! ## Overview
//!
//! The `Settings` structure represents the configuration settings for the rampdelay PAM module.
//! It includes fields such as `action`, `user`, `tally_dir`, `free_tries`, `base_delay_seconds`,
//! and `ramp_multiplier`.
//!
//! ## License
//!
//! pam-authramp
//! Copyright (C) 2023 github.com/34N0
//!
//! This program is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! This program is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::Actions;
use ini::Ini;
use pam::constants::{PamFlag, PamResultCode};
Expand All @@ -9,17 +39,26 @@ use users::User;
const DEFAULT_TALLY_DIR: &str = "/var/run/rampdelay";
const DEFAULT_CONFIG_FILE_PATH: &str = "/etc/security/rampdelay.conf";


// Settings struct represents the configuration loaded from default values, configuration file and parameters
#[derive(Debug)]
pub struct Settings {
// PAM action
pub action: Option<Actions>,
// PAM user
pub user: Option<User>,
// Directory where tally information is stored.
pub tally_dir: PathBuf,
// Number of allowed free authentication attempts before applying delays.
pub free_tries: i32,
// Base delay applied to each authentication failure.
pub base_delay_seconds: i32,
// Multiplier for the delay calculation based on the number of failures.
pub ramp_multiplier: i32,
}

impl Default for Settings {
/// Creates a default 'Settings' struct. Default configruation values are set here.
fn default() -> Self {
Settings {
tally_dir: PathBuf::from(DEFAULT_TALLY_DIR),
Expand All @@ -33,15 +72,20 @@ impl Default for Settings {
}

impl Settings {
pub fn build(
user: Option<User>,
args: Vec<&CStr>,
_flags: PamFlag,
_config_file: Option<PathBuf>,
) -> Result<Settings, PamResultCode> {
// Load INI file.
let mut settings = Ini::load_from_file(
_config_file.unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_FILE_PATH)),
/// Loads configuration settings from an INI file, returning a `Settings` instance.
///
/// # Arguments
///
/// * `config_file`: An optional `PathBuf` specifying the path to the INI file. If
/// not provided, the default configuration file path is used.
///
/// # Returns
///
/// A `Settings` instance populated with values from the configuration file, or the
/// default values if the file is not present or cannot be loaded.
fn load_conf_file(config_file: Option<PathBuf>) -> Settings {
Ini::load_from_file(
config_file.unwrap_or(PathBuf::from(DEFAULT_CONFIG_FILE_PATH)),
)
.ok()
.and_then(|ini| ini.section(Some("Settings")).cloned())
Expand All @@ -64,7 +108,34 @@ impl Settings {
.unwrap_or_default(),
..Settings::default()
})
.unwrap_or_default();
.unwrap_or_default()
}

/// Constructs a `Settings` instance based on input parameters, including user
/// information, PAM flags, and an optional configuration file path.
///
/// # Arguments
///
/// * `user`: An optional `User` instance representing the user associated with
/// the PAM session.
/// * `args`: A vector of CStr references representing the PAM module arguments.
/// * `_flags`: PAM flags indicating the context of the PAM operation (unused).
/// * `config_file`: An optional `PathBuf` specifying the path to the INI file. If
/// not provided, the default configuration file path is used.
///
/// # Returns
///
/// A `Result` containing the constructed `Settings` instance or a `PamResultCode`
/// indicating an error during the construction process.
pub fn build(
user: Option<User>,
args: Vec<&CStr>,
_flags: PamFlag,
config_file: Option<PathBuf>,
) -> Result<Settings, PamResultCode> {

// Load INI file.
let mut settings = Self::load_conf_file(config_file);

// create possible action collection
let action_map: HashMap<&str, Actions> = [
Expand Down Expand Up @@ -93,6 +164,7 @@ impl Settings {
}
}

// Unit Tests
#[cfg(test)]
mod tests {
use super::*;
Expand Down
54 changes: 54 additions & 0 deletions src/tally.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,61 @@
//! # Tally Module
//!
//! The `tally` module manages the account lockout status, including the number of authentication
//! failures and the timestamp of the last failure. It provides functionality to open and update
//! the tally based on the authentication actions performed.
//!
//! ## Overview
//!
//! The `Tally` struct represents the account lockout information, and the module provides
//! functionality to interact with this information, updating it based on authentication results.
//!
//! ## Tally Structure
//!
//! The `Tally` struct has the following fields:
//!
//! - `tally_file`: An optional `PathBuf` representing the path to the file storing tally information.
//! - `failures_count`: An integer representing the number of authentication failures.
//! - `failure_instant`: A `DateTime<Utc>` representing the timestamp of the last authentication failure.
//!
//! ## License
//!
//! pam-authramp
//! Copyright (C) 2023 github.com/34N0
//!
//! This program is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! This program is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::{fs, path::PathBuf};

use crate::{settings::Settings, Actions};
use chrono::{DateTime, Utc};
use ini::Ini;
use pam::constants::PamResultCode;

/// The `Tally` struct represents the account lockout information, including
/// the number of authentication failures and the timestamp of the last failure.
#[derive(Debug, PartialEq)]
pub struct Tally {
/// An optional `PathBuf` representing the path to the file storing tally information.
pub tally_file: Option<PathBuf>,
/// An integer representing the number of authentication failures.
pub failures_count: i32,
/// A `DateTime<Utc>` representing the timestamp of the last authentication failure.
pub failure_instant: DateTime<Utc>,
}

impl Default for Tally {
/// Creates a default `Tally` instance with zero failures and the current timestamp.
fn default() -> Self {
Tally {
tally_file: None,
Expand All @@ -23,6 +66,16 @@ impl Default for Tally {
}

impl Tally {
/// Opens or creates the tally file based on the provided `Settings`.
///
/// If the file exists, loads the values; if not, creates the file with default values.
/// Updates the tally based on authentication actions, such as successful or failed attempts.
///
/// # Arguments
/// - settings: Settings struct
///
/// # Returns
/// Tally struct or PAM_AUTH_ERR
pub fn open(settings: &Settings) -> Result<Self, PamResultCode> {
let mut tally = Tally::default();
let user = settings.user.as_ref().ok_or(PamResultCode::PAM_AUTH_ERR)?;
Expand Down Expand Up @@ -100,6 +153,7 @@ impl Tally {
}
}

// Unit Tests
#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading

0 comments on commit c764566

Please sign in to comment.