kftray-helper 0.27.30

Privileged helper binary for KFTray
Documentation
use std::time::{
    SystemTime,
    UNIX_EPOCH,
};

use log::{
    debug,
    info,
};

use crate::error::HelperError;
use crate::messages::HelperRequest;

const VALID_APP_IDS: &[&str] = &["com.kftray.app", "com.hcavarsan.kftray"];

const MAX_TIMESTAMP_SKEW_SECONDS: u64 = 300;

pub fn validate_request(request: &HelperRequest) -> Result<(), HelperError> {
    validate_app_id(&request.app_id)?;
    validate_timestamp(request.timestamp)?;
    Ok(())
}

fn validate_app_id(app_id: &str) -> Result<(), HelperError> {
    if VALID_APP_IDS.contains(&app_id) {
        Ok(())
    } else {
        Err(HelperError::Authentication(format!(
            "Invalid app_id: {app_id}"
        )))
    }
}

fn validate_timestamp(timestamp: u64) -> Result<(), HelperError> {
    let current_time = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|_| HelperError::Authentication("Failed to get current time".to_string()))?
        .as_secs();

    let time_diff = current_time.abs_diff(timestamp);

    if time_diff > MAX_TIMESTAMP_SKEW_SECONDS {
        Err(HelperError::Authentication(format!(
            "Request timestamp too far from current time: {time_diff} seconds"
        )))
    } else {
        Ok(())
    }
}

#[cfg(target_os = "linux")]
pub fn validate_peer_credentials(
    stream: &std::os::unix::net::UnixStream,
) -> Result<(), HelperError> {
    use std::os::fd::AsRawFd;

    let socket_fd = stream.as_raw_fd();
    let mut cred = libc::ucred {
        pid: 0,
        uid: 0,
        gid: 0,
    };
    let mut cred_len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;

    let result = unsafe {
        libc::getsockopt(
            socket_fd,
            libc::SOL_SOCKET,
            libc::SO_PEERCRED,
            &mut cred as *mut _ as *mut libc::c_void,
            &mut cred_len,
        )
    };

    if result != 0 {
        return Err(HelperError::Authentication(
            "Failed to get peer credentials".to_string(),
        ));
    }

    let current_uid = unsafe { libc::getuid() };
    let authorized_uid = get_authorized_user_uid();

    if current_uid == 0 {
        if cred.uid == authorized_uid {
            debug!(
                "Peer credentials validated (root accepting authorized user): UID={}, GID={}, PID={}",
                cred.uid, cred.gid, cred.pid
            );
            return Ok(());
        } else {
            return Err(HelperError::Authentication(format!(
                "Peer UID {} is not authorized (expected UID {})",
                cred.uid, authorized_uid
            )));
        }
    }

    if cred.uid != current_uid {
        return Err(HelperError::Authentication(format!(
            "Peer UID {} does not match expected UID {}",
            cred.uid, current_uid
        )));
    }

    debug!(
        "Peer credentials validated: UID={}, GID={}, PID={}",
        cred.uid, cred.gid, cred.pid
    );

    Ok(())
}

#[cfg(target_os = "macos")]
pub fn validate_peer_credentials(
    stream: &std::os::unix::net::UnixStream,
) -> Result<(), HelperError> {
    use std::os::fd::AsRawFd;

    let socket_fd = stream.as_raw_fd();
    let mut cred = libc::xucred {
        cr_version: 0,
        cr_uid: 0,
        cr_ngroups: 0,
        cr_groups: [0; 16],
    };
    let mut cred_len = std::mem::size_of::<libc::xucred>() as libc::socklen_t;

    let result = unsafe {
        libc::getsockopt(
            socket_fd,
            0,
            1,
            &mut cred as *mut _ as *mut libc::c_void,
            &mut cred_len,
        )
    };

    if result != 0 {
        return Err(HelperError::Authentication(
            "Failed to get peer credentials".to_string(),
        ));
    }

    let current_uid = unsafe { libc::getuid() };
    let authorized_uid = get_authorized_user_uid();

    debug!(
        "Current UID: {}, Peer UID: {}, Authorized UID: {}",
        current_uid, cred.cr_uid, authorized_uid
    );

    if current_uid == 0 {
        if cred.cr_uid == authorized_uid {
            info!(
                "Peer credentials validated (root accepting authorized user): UID={}",
                cred.cr_uid
            );
            return Ok(());
        } else {
            return Err(HelperError::Authentication(format!(
                "Peer UID {} is not authorized (expected UID {})",
                cred.cr_uid, authorized_uid
            )));
        }
    }

    if cred.cr_uid != current_uid {
        return Err(HelperError::Authentication(format!(
            "Peer UID {} does not match expected UID {}",
            cred.cr_uid, current_uid
        )));
    }

    debug!("Peer credentials validated: UID={}", cred.cr_uid);

    Ok(())
}

#[cfg(unix)]
fn get_authorized_user_uid() -> u32 {
    if let Ok(sudo_uid) = std::env::var("SUDO_UID")
        && let Ok(uid) = sudo_uid.parse::<u32>()
    {
        info!("Found authorized UID from SUDO_UID: {uid}");
        return uid;
    }

    if let Ok(socket_path) = crate::communication::get_default_socket_path()
        && let Ok(metadata) = std::fs::metadata(&socket_path)
    {
        use std::os::unix::fs::MetadataExt;
        let owner_uid = metadata.uid();
        if owner_uid != 0 {
            info!("Found authorized UID from socket file ownership: {owner_uid}");
            return owner_uid;
        }
    }

    if let Ok(socket_path) = crate::communication::get_default_socket_path()
        && let Some(parent_dir) = socket_path.parent()
        && let Ok(metadata) = std::fs::metadata(parent_dir)
    {
        use std::os::unix::fs::MetadataExt;
        let owner_uid = metadata.uid();
        if owner_uid != 0 {
            info!("Found authorized UID from socket directory ownership: {owner_uid}");
            return owner_uid;
        }
    }

    let current_uid = unsafe { libc::getuid() };
    info!("No specific authorized UID found, falling back to current UID: {current_uid}");
    current_uid
}

#[cfg(windows)]
pub fn validate_peer_credentials(
    _pipe_handle: windows::Win32::Foundation::HANDLE,
) -> Result<(), HelperError> {
    Ok(())
}

#[cfg(not(any(unix, windows)))]
fn get_authorized_user_uid() -> u32 {
    warn!("Unsupported platform: No UID-based authorization, using default");
    0
}

#[cfg(not(any(unix, windows)))]
pub fn validate_peer_credentials<T>(_stream: &T) -> Result<(), HelperError> {
    warn!("Unsupported platform: Peer credential validation skipped");
    Ok(())
}