crates/veilvoice-watch/src/privilege.rs

what this file is for · veilvoice-watch · 404 lines · the same file on GitHub

The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.


// SPDX-License-Identifier: GPL-3.0-or-later
//! What privilege VeilVoice is running with, and what each level can actually
//! see.
//!
//! # Three levels, and the third one this project does not ship
//!
//! * [`Level::User`] is VeilVoice as you. Everything the de-identifier does
//!   happens here, and nothing about the engine, the container format or the
//!   app lock needs any more than this.
//! * [`Level::Elevated`] is running as administrator or root. The monitoring
//!   features see further: processes belonging to other users, service
//!   accounts, and a few registry and system paths that are unreadable
//!   otherwise.
//! * **Kernel level** is not shipped, and not for want of trying. Loading a
//!   kernel driver on 64-bit Windows needs an EV code-signing certificate
//!   issued to a verified legal entity plus Microsoft's attestation signing;
//!   macOS needs an Apple Developer ID and an entitlement granted case by
//!   case. Both are identity checks, and this project is published under a
//!   pseudonym on purpose. [`NO_KERNEL`] says so in the words a front end
//!   should show.
//!
//! # This crate does not elevate anything
//!
//! It reports. It does not re-launch VeilVoice as administrator, install a
//! service, or ask for a password. Those are changes to somebody's machine and
//! they belong to the person whose machine it is: [`Level::how_to_raise`]
//! prints the command, and they type it.
//!
//! That is not caution for its own sake. A privacy tool that silently acquires
//! administrator rights is a privacy tool nobody can reason about, and one that
//! installs a background service without being asked is worse, because a service
//! outlives the window it was started from, and somebody who tried VeilVoice
//! once should not find it still running next month.
//!
//! # Detection is a measurement, and it can fail
//!
//! There is no `am_i_admin()` in the standard library and reaching the real
//! answer is FFI on every platform here. So this asks a tool the system already
//! ships, exactly as `veilvoice-watch` asks the registry, and when the tool
//! cannot be run, the answer is [`Level::Unknown`] rather than a guess.
//!
//! **`Unknown` is not `User`.** Reporting "not elevated" when the truth is "I
//! could not tell" would understate what VeilVoice can see, which sounds like
//! the safe direction and is not: somebody would conclude a feature is
//! unavailable and stop looking at its output.
//!
//! # In plain words
//!
//! Most of VeilVoice needs no special permissions at all, because changing a voice is
//! something any program can do with your own account.
//!
//! The parts that *watch* your machine can see more when VeilVoice is run as an
//! administrator: programs belonging to other accounts, and a few places on the
//! system that are otherwise off limits. This tells you which of those you are
//! currently getting, and how to run it the other way if you want to.
//!
//! It will not do that for you. Running as administrator, or installing a
//! background service, is a change to your computer and it should be one you
//! made on purpose. And there is a third level, inside the operating system
//! itself, that VeilVoice does not reach and says so rather than implying it
//! does.

use std::process::Command;


/// What VeilVoice is running with.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Level {
    /// An ordinary user account.
    #[default]
    User,
    /// Administrator on Windows, root elsewhere.
    Elevated,
    /// The probe could not run.
    ///
    /// Deliberately not [`Level::User`]. "I could not tell" and "you are not
    /// elevated" lead to different actions, and reporting the second when the
    /// first is true understates what VeilVoice can see, which sounds like
    /// the cautious direction and is not.
    Unknown,
}


impl Level {

    /// A short name.
    pub fn label(self) -> &'static str {
        match self {
            Self::User => "your own account",
            Self::Elevated => {
                if cfg!(windows) {
                    "administrator"
                } else {
                    "root"
                }
            }
            Self::Unknown => "could not tell",
        }
    }



    /// What this level can see, and what it cannot.
    pub fn what_it_sees(self) -> &'static str {
        match self {
            Self::User => {
                "Everything VeilVoice does to audio happens here and needs nothing \
                 more. The monitoring features see programs running as you, and not \
                 usually much else -- so something running as another user or as a \
                 service may not appear at all."
            }
            Self::Elevated => {
                "The monitoring features can see processes belonging to other users \
                 and to service accounts, and can read system locations that are \
                 otherwise refused. The de-identifier itself does nothing different: \
                 it never needed this."
            }
            Self::Unknown => {
                "The check could not be run, so this does not know. That is not the \
                 same as running unprivileged: treat the monitoring output as \
                 whatever it says it is, and do not assume a short list means a \
                 quiet machine."
            }
        }
    }



    /// The command that would run VeilVoice at the higher level.
    ///
    /// Returned as text to print, never run. Elevating is a change to somebody's
    /// machine and it belongs to them.
    pub fn how_to_raise(self) -> Option<&'static str> {
        match self {
            Self::Elevated => None,
            _ if cfg!(windows) => Some(
                "Right-click VeilVoice and choose \"Run as administrator\", or from an \
                 elevated PowerShell: Start-Process veilvoice -Verb RunAs",
            ),
            _ => Some("Run it under sudo: sudo veilvoice ..."),
        }
    }



    /// Whether the monitoring features are seeing everything they could.
    pub fn is_full_view(self) -> bool {
        self == Self::Elevated
    }

}


/// What VeilVoice is running with right now.
///
/// Asks the system's own tool. Never elevates, never prompts, changes nothing.
pub fn level() -> Level {
    #[cfg(windows)]
    {
        windows_level()
    }
    #[cfg(not(windows))]
    {
        unix_level()
    }
}



/// On Windows, ask `whoami /groups` for the administrators SID.
///
/// `S-1-5-32-544` is the built-in Administrators group, and the well-known SID
/// is used rather than the group's *name*, which is translated on a localised
/// system and would make this answer "not elevated" on every machine that is
/// not in English.
#[cfg(windows)]
fn windows_level() -> Level {
    // Absolute path, never a bare name: Windows searches the current directory
    // before most of PATH, so a `whoami.exe` in the folder VeilVoice was
    // unpacked into would answer this question instead. This is a security
    // tool asking what privilege it holds.
    let root = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
    let program = format!(r"{root}\System32\whoami.exe");

    let mut command = Command::new(&program);
    command.args(["/groups"]);
    {
        use std::os::windows::process::CommandExt;
        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
        command.creation_flags(CREATE_NO_WINDOW);
    }
    let output = match command.output() {
        Ok(output) if output.status.success() => output,
        _ => return Level::Unknown,
    };
    let text = String::from_utf8_lossy(&output.stdout);
    if !text.contains("S-1-5-32-544") {
        // The group is not even in the token: an ordinary account.
        return Level::User;
    }
    // Present, but a filtered token lists it as "Group used for deny only",
    // which is what an un-elevated administrator account looks like. Being a
    // member of Administrators is not the same as running as one, and getting
    // this wrong would overstate what VeilVoice can see -- the dangerous
    // direction.
    //
    // Both parts are on one line. `whoami /groups` pads its columns to about
    // 236 characters, so a console displaying it wraps and it *looks* like two
    // rows; reading it that way and matching only the first would report every
    // administrator account as elevated whether or not it was. Measured on a
    // machine in exactly that state: the account is in Administrators, the
    // shell is not elevated, and this reports "your own account".
    for line in text.lines() {
        if line.contains("S-1-5-32-544") {
            if line.contains("Group used for deny only") {
                return Level::User;
            }
            return Level::Elevated;
        }
    }
    Level::User
}



/// Everywhere else, ask `id -u`.
#[cfg(not(windows))]
fn unix_level() -> Level {
    let output = match Command::new("/usr/bin/id").arg("-u").output() {
        Ok(output) if output.status.success() => output,
        // Not every system puts it there. One fallback, through PATH, and then
        // the answer is that this does not know.
        _ => match Command::new("id").arg("-u").output() {
            Ok(output) if output.status.success() => output,
            _ => return Level::Unknown,
        },
    };
    match String::from_utf8_lossy(&output.stdout)
        .trim()
        .parse::<u32>()
    {
        Ok(0) => Level::Elevated,
        Ok(_) => Level::User,
        Err(_) => Level::Unknown,
    }
}



/// Whether a background service is installed.
///
/// Always `false`, and the function exists so that a front end asking
/// the question gets an answer rather than a missing feature: **VeilVoice does
/// not install a service.** [`NO_SERVICE`] is why.
pub fn service_installed() -> bool {
    false
}



/// Why the opt-in service is not shipped, in the words to show.
pub const NO_SERVICE: &str = "\
VeilVoice does not install a background service. A service outlives the window \
it was started from, starts itself at boot, and runs whether or not anybody is \
using the program -- so somebody who tried VeilVoice once should not find it \
still running next month. Where continuous monitoring is wanted, run VeilVoice \
and leave it open: what it can see is then exactly what it says it can see, and \
closing the window ends it.";



/// What kernel level would need, and why it is not here.
pub const NO_KERNEL: &str = "\
VeilVoice does not reach inside the operating system, and cannot. A kernel \
driver on 64-bit Windows needs an EV code-signing certificate issued to a \
verified legal entity and then Microsoft's attestation signing; macOS needs an \
Apple Developer ID and an entitlement Apple grants case by case. Both are \
identity checks on a named legal person, and this project is published under a \
pseudonym on purpose. So the monitoring here is what a program running as you, \
or as an administrator, can observe from outside -- which is real, and is less \
than a driver would see.";



/// What this crate will not do, and why that is deliberate.
pub const NEVER_ELEVATES: &str = "\
This never raises its own privileges, installs anything, or asks for a \
password. It reports what VeilVoice is running with and prints the command that \
would run it differently. A privacy tool that silently acquires administrator \
rights is a privacy tool nobody can reason about.";


#[cfg(test)]
mod tests {
    use super::*;

    /// "I could not tell" must never be reported as "not elevated".
    #[test]
    fn unknown_is_its_own_answer_and_not_the_unprivileged_one() {
        assert_ne!(Level::Unknown, Level::User);
        assert!(!Level::Unknown.is_full_view());
        let words = Level::Unknown.what_it_sees().to_lowercase();
        assert!(words.contains("does not know"), "{words}");
        assert!(
            words.contains("not the same as running unprivileged"),
            "the distinction has to be stated, not implied: {words}"
        );
        assert!(
            words.contains("do not assume a short list"),
            "and the action it changes: {words}"
        );
    }

    /// Every level says what it can see, in enough words to be useful.
    #[test]
    fn every_level_explains_itself() {
        for level in [Level::User, Level::Elevated, Level::Unknown] {
            assert!(!level.label().is_empty(), "{level:?}");
            assert!(level.what_it_sees().len() > 80, "{level:?}");
        }
        // And the user level has to say the de-identifier needs nothing more,
        // or somebody runs the whole thing as root for no reason.
        assert!(Level::User.what_it_sees().contains("needs nothing more"));
        assert!(Level::Elevated
            .what_it_sees()
            .contains("it never needed this"));
    }

    /// Elevating is a command to print, never an action to take.
    #[test]
    fn raising_privilege_is_something_the_reader_does() {
        assert_eq!(Level::Elevated.how_to_raise(), None, "already there");
        for level in [Level::User, Level::Unknown] {
            let how = level.how_to_raise().expect("a command to print");
            assert!(how.len() > 20, "{how}");
        }

        // Nothing in this module *runs* an elevation. Checked by naming every
        // subprocess it starts rather than by searching for words: the strings
        // above legitimately contain "RunAs" and "sudo" because they are the
        // commands a reader is told to type, and the first version of this test
        // flagged them. An honest failure, and the wrong question -- what
        // matters is what is executed, so that is what is counted.
        let source = include_str!("privilege.rs").replace("\r\n", "\n");
        let body = source.split("#[cfg(test)]").next().unwrap().to_string();
        let marker = "Command::new(";
        let started: Vec<String> = body
            .match_indices(marker)
            .map(|(at, _)| {
                let rest = &body[at + marker.len()..];
                rest.split(')').next().unwrap_or("").trim().to_string()
            })
            .collect();
        assert!(!started.is_empty(), "the probe has to run something");
        for program in &started {
            assert!(
                program == "&program" || program.contains("id"),
                "this module starts {program}, which is neither of the two probes"
            );
        }
    }

    /// The three scope notes each say the thing outright rather than hinting.
    #[test]
    fn the_limits_are_stated_rather_than_implied() {
        let kernel = NO_KERNEL.to_lowercase();
        assert!(kernel.contains("does not reach inside"), "{kernel}");
        assert!(kernel.contains("pseudonym"), "{kernel}");
        assert!(
            kernel.contains("less than a driver would see"),
            "the comparison a reader needs: {kernel}"
        );

        let service = NO_SERVICE.to_lowercase();
        assert!(service.contains("does not install a background service"));
        assert!(
            service.contains("still running next month"),
            "the concrete case: {service}"
        );
        assert!(service.contains("closing the window ends it"));

        let never = NEVER_ELEVATES.to_lowercase();
        assert!(never.contains("never raises its own privileges"));
        assert!(never.contains("nobody can reason about"));
    }

    /// No service is installed, and the function that says so is honest about
    /// it rather than absent.
    #[test]
    fn there_is_no_service_and_the_answer_says_so() {
        assert!(!service_installed());
    }

    /// Asking the real machine must not panic, hang, prompt, or change
    /// anything.
    #[test]
    fn asking_is_safe_wherever_this_runs() {
        let level = level();
        assert!(!level.label().is_empty());
        assert!(!level.what_it_sees().is_empty());
        // Whatever it is, the two that mean "not the full view" agree about it.
        assert_eq!(level.is_full_view(), level == Level::Elevated);
    }

    /// The Windows probe keys on the well-known SID, not the group's name.
    ///
    /// The name is translated on a localised system, so matching "Administrators"
    /// would report every non-English machine as unprivileged -- a wrong answer
    /// that would only ever be seen by people this project is unlikely to hear
    /// from.
    #[test]
    fn the_windows_probe_uses_the_sid_rather_than_a_translated_name() {
        let source = include_str!("privilege.rs").replace("\r\n", "\n");
        assert!(source.contains("S-1-5-32-544"));
        let code: String = source
            .lines()
            .filter(|line| {
                let trimmed = line.trim_start();
                !trimmed.starts_with("//") && !trimmed.starts_with("///")
            })
            .collect::<Vec<_>>()
            .join("\n");
        assert!(
            !code.contains("contains(\"Administrators\")"),
            "the group name is localised; the SID is not"
        );
    }
}