Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/botloftd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ futures-util = "0.3.34"
getrandom = "0.4.3"
hex = "0.4.3"
jiff = "0.2.37"
jpeg-encoder = "0.7.1"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
sha2 = "0.11.0"
Expand All @@ -36,7 +37,7 @@ tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
workspace = true

[target."cfg(windows)".dependencies]
windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading", "Win32_System_Memory", "Win32_System_JobObjects", "Win32_System_Environment", "Win32_System_Power", "Win32_System_Console", "Win32_System_Com", "Win32_System_Ole", "Win32_System_Variant", "Win32_System_TaskScheduler", "Win32_System_RemoteDesktop", "Win32_Security_Authentication_Identity", "Win32_UI_Shell", "Win32_UI_Shell_Common"] }
windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading", "Win32_System_Memory", "Win32_System_JobObjects", "Win32_System_Environment", "Win32_System_Power", "Win32_System_Console", "Win32_System_Com", "Win32_System_Ole", "Win32_System_Variant", "Win32_System_TaskScheduler", "Win32_System_RemoteDesktop", "Win32_Security_Authentication_Identity", "Win32_UI_Shell", "Win32_UI_Shell_Common", "Win32_UI_WindowsAndMessaging", "Win32_UI_HiDpi", "Win32_Graphics_Gdi", "Win32_Graphics_Dwm", "Win32_Storage_FileSystem", "Win32_Storage_Xps", "Win32_System_LibraryLoader"] }
# Named by the code that `windows::core::implement` writes.
windows-core = "0.62.2"

Expand Down
209 changes: 209 additions & 0 deletions crates/botloftd/src/platform/desktop/blocked.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//! What is never granted on the desktop, whatever the owner allows (spec
//! 24.3): Botloft itself, windows running as administrator, terminals and
//! command boxes, and password managers. The secure desktop (UAC, the lock
//! screen) has no windows here at all. Password fields are left out where
//! a window is read.

use super::Window;

/// Why a window is never granted.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Never {
/// A bot never approves itself.
Botloft,
/// It runs with more rights than the daemon.
Elevated,
/// Typing there would skip the bot's rules for commands.
Terminal,
/// It keeps passwords or the computer's security.
Passwords,
}

impl Never {
/// What the bot reads about it.
pub fn why(self) -> &'static str {
match self {
Self::Botloft => "it is Botloft itself",
Self::Elevated => "it runs as administrator",
Self::Terminal => {
"it runs commands, and you have your own tools for that, under your rules"
}
Self::Passwords => "it keeps passwords or the computer's security",
}
}
}

const BOTLOFT: &[&str] = &[
"botloft.exe",
"botloftd.exe",
"botloft-app.exe",
"botloft-setup.exe",
];

/// Terminals, command boxes and the tools that change the system under
/// them.
const TERMINALS: &[&str] = &[
"cmd.exe",
"powershell.exe",
"powershell_ise.exe",
"pwsh.exe",
"windowsterminal.exe",
"wt.exe",
"openconsole.exe",
"conhost.exe",
"wsl.exe",
"wslhost.exe",
"bash.exe",
"mintty.exe",
"alacritty.exe",
"wezterm-gui.exe",
"regedit.exe",
"mmc.exe",
"taskmgr.exe",
];

const PASSWORDS: &[&str] = &[
"1password.exe",
"bitwarden.exe",
"keepass.exe",
"keepassxc.exe",
"lastpass.exe",
"dashlane.exe",
"credentialuibroker.exe",
"sechealthui.exe",
"securityhealthsystray.exe",
"consent.exe",
"logonui.exe",
];

/// The Credential Manager is a page of the Control Panel, an Explorer
/// window: known by its title, in the languages Botloft speaks.
const CREDENTIALS: &[&str] = &[
"credential manager",
"gerenciador de credenciais",
"administrador de credenciales",
];

/// The class of a dialog box: Explorer's are the Run box (Win+R).
const DIALOG: &str = "#32770";

/// Why `window` is never granted, if it is not.
pub fn never(window: &Window) -> Option<Never> {
// By hand: a Windows path read on another system has no separators.
let path = window.app.path.to_string_lossy().to_lowercase();
let file = path
.rsplit(['\\', '/'])
.next()
.unwrap_or_default()
.to_owned();
let named = |list: &[&str]| list.contains(&file.as_str());
if named(BOTLOFT) {
return Some(Never::Botloft);
}
if window.elevated {
return Some(Never::Elevated);
}
if named(TERMINALS) || (file == "explorer.exe" && window.class == DIALOG) {
return Some(Never::Terminal);
}
let title = window.title.to_lowercase();
if named(PASSWORDS)
|| (file == "explorer.exe" && CREDENTIALS.iter().any(|name| title.contains(name)))
{
return Some(Never::Passwords);
}
None
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;

use super::super::App;
use super::*;

fn window(path: &str, title: &str, class: &str) -> Window {
Window {
id: 1,
title: title.to_owned(),
class: class.to_owned(),
app: App {
path: PathBuf::from(path),
name: String::new(),
},
minimized: false,
elevated: false,
}
}

#[test]
fn everyday_apps_may_be_granted() {
for path in [
r"C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE",
r"C:\Windows\System32\notepad.exe",
r"C:\Users\ana\AppData\Local\Clinic\clinic.exe",
] {
assert_eq!(never(&window(path, "Book1", "XLMAIN")), None, "{path}");
}
// An ordinary Explorer folder window too.
let folder = window(r"C:\Windows\explorer.exe", "Downloads", "CabinetWClass");
assert_eq!(never(&folder), None);
}

#[test]
fn botloft_terminals_and_passwords_never_are() {
let cases = [
(
r"C:\Users\ana\AppData\Local\Botloft\Botloft.exe",
Never::Botloft,
),
(r"C:\Windows\System32\cmd.exe", Never::Terminal),
(
r"C:\Program Files\WindowsApps\Microsoft.WindowsTerminal\WindowsTerminal.exe",
Never::Terminal,
),
(r"C:\Program Files\PowerShell\7\pwsh.exe", Never::Terminal),
(r"C:\Windows\regedit.exe", Never::Terminal),
(r"C:\Windows\System32\Taskmgr.exe", Never::Terminal),
(
r"C:\Program Files\KeePassXC\KeePassXC.exe",
Never::Passwords,
),
(
r"C:\Program Files\Bitwarden\Bitwarden.exe",
Never::Passwords,
),
(
r"C:\Windows\SystemApps\Microsoft.SecHealthUI\SecHealthUI.exe",
Never::Passwords,
),
];
for (path, why) in cases {
assert_eq!(never(&window(path, "Any", "Any")), Some(why), "{path}");
}
}

#[test]
fn the_run_box_and_the_credential_manager_never_are() {
let explorer = r"C:\Windows\explorer.exe";
assert_eq!(
never(&window(explorer, "Executar", DIALOG)),
Some(Never::Terminal)
);
assert_eq!(
never(&window(
explorer,
"Gerenciador de Credenciais",
"CabinetWClass"
)),
Some(Never::Passwords)
);
}

#[test]
fn a_window_running_as_administrator_never_is() {
let mut admin = window(r"C:\Windows\System32\notepad.exe", "Notes", "Notepad");
admin.elevated = true;
assert_eq!(never(&admin), Some(Never::Elevated));
}
}
100 changes: 100 additions & 0 deletions crates/botloftd/src/platform/desktop/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! The owner's desktop (spec 24): the windows open on their screen, which
//! app each one is from, what is never granted, and a picture of a window.
//! Only on Windows; elsewhere every call says the desktop is not there.

mod blocked;
// Only Windows takes pictures; the tests run everywhere.
#[cfg_attr(not(windows), allow(dead_code))]
mod shrink;
#[cfg(windows)]
mod windows;

use std::path::PathBuf;

pub use blocked::{Never, never};

/// The largest picture of a window the bot gets, in logical pixels (24.4).
pub const PICTURE_MAX: (u32, u32) = (1600, 1200);
/// The JPEG quality of a picture, as `browser_screenshot`'s.
#[cfg_attr(not(windows), allow(dead_code))]
const QUALITY: u8 = 70;

/// A program on the owner's computer, by its executable (spec 24.2).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct App {
pub path: PathBuf,
/// The description in the file's version info ("Microsoft Excel"), or
/// the file's name without `.exe`.
pub name: String,
}

/// A window open on the owner's desktop.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Window {
/// The window's handle, as the number the bot names it by.
pub id: u64,
pub title: String,
/// The window class, which tells some system windows apart.
pub class: String,
pub app: App,
pub minimized: bool,
/// Its process runs with more rights than the daemon (as administrator),
/// or its rights could not be read.
pub elevated: bool,
}

/// A picture of a window, as a JPEG.
#[derive(Debug, Clone)]
pub struct Picture {
pub jpeg: Vec<u8>,
pub width: u32,
pub height: u32,
}

#[derive(Debug, thiserror::Error)]
pub enum DesktopError {
#[error("the desktop is not available on this system yet")]
Unavailable,
#[error("that window is not open anymore")]
Gone,
#[error("that window is minimized, so there is nothing to see in it")]
Minimized,
#[error("Windows could not do it: {0}")]
System(String),
}

/// The windows open on the owner's desktop: shown ones, minimized too,
/// with a title, from every app (spec 24.4). What the bot may see of them
/// is the caller's to decide.
pub fn windows() -> Result<Vec<Window>, DesktopError> {
#[cfg(windows)]
{
windows::list()
}
#[cfg(not(windows))]
{
Err(DesktopError::Unavailable)
}
}

/// A picture of the window `id`, only of it, even behind other windows, in
/// logical pixels up to [`PICTURE_MAX`] (spec 24.4).
pub fn picture(id: u64) -> Result<Picture, DesktopError> {
#[cfg(windows)]
{
let (rgb, width, height, scale) = windows::capture(id)?;
Ok(shrink::picture(
&rgb,
width,
height,
scale,
PICTURE_MAX,
QUALITY,
))
}
#[cfg(not(windows))]
{
let _ = id;
Err(DesktopError::Unavailable)
}
}
Loading
Loading