lukeflo
lukeflo

Reputation: 175

Spawn command as root and authenticate with Polkit/rpassword

Outdated first approach

I'm trying to spawn a command with sudo and pass the password to the process via rpassword's BufRead implementation.

To not prompt for the password on the TTY I use the -S flag for sudo. When spawning the command I take() the stdin, spawn another thread and write the via BufRead saved password to the stdin; as suggested in the docs.

Here is the example code:

use rpassword::read_password_from_bufread;
use std::{
    io::{Cursor, Write},
    process::{Command, Stdio},
    thread,
};

fn sudo_cmd(pw: String) {
    let mut cmd = Command::new("sudo")
        .arg("-S")
        .arg("ls")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        // .stderr(Stdio::null()) //<<== should hide password prompt
        .spawn()
        .ok()
        .expect("not spawned");

    let mut stdin = cmd.stdin.take().expect("Couldnt take stdin");

    thread::spawn(move || {
        stdin
            .write_all(pw.as_bytes())
            .expect("Couldnt write stding");
    });

    let output = cmd.wait_with_output().expect("wheres the output");

    println!(
        "Output:\n{}",
        String::from_utf8(output.stdout).expect("Cant read stdout")
    );
}

fn main() {
    let mut mock_input = Cursor::new("my-password\n".as_bytes().to_owned());
    let password = read_password_from_bufread(&mut mock_input).unwrap();
    sudo_cmd(password);
}

Unfortunately, that doesn't work. The process waits for a second, then exits as if no password was provided:

   Compiling testproject v0.1.0 (/home/lukeflo/Documents/projects/coding/testfiles/rust-tests/testproject)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/testproject`
Password: Sorry, try again.
Password: 
sudo: no password was provided
sudo: 1 incorrect password attempt
Output:

Beside concerns regarding security, what is the correct way to get that done? I can't/wont use the TTY prompt directly (which would be possible for a plain CLI app), because I want to understand how the password can be collected "indirect"; as some GUI wrapper for e.g. pw manager do.

EDIT: Using polkit

For GUI/TUI Polkit seems to be the way to got. Unfortunately, AFAIK Rust libraries for Polkit interaction are rare and bad documented. Or I might have overlooked something. Happy for advice with those stuff. The general code should remain the same. I just want to authenticate the sudo command with polkit.

Upvotes: 0

Views: 95

Answers (1)

Fumnanya
Fumnanya

Reputation: 51

First of all, your sudo approach works fine; the reason the process fails is because you're giving it a password value of "my-password", which (almost certainly) isn't your actual password.

If you uncommented the .stderr(Stdio::null()) bit (so the password prompt is hidden), and changed main() to look more like this:

fn main() {
    let password = rpassword::prompt_password("Your password: ").unwrap();
    sudo_cmd(password);
}

Then cargo run would prompt you for your password (which would be hidden), and then display the contents of the directory if you input the correct password.

rpassword's documentation says read_password_from_bufread is for unit-testing purposes:

Finally, in unit tests, you might want to pass a Cursor, which implements BufRead. In that case, you can use read_password_from_bufread and prompt_password_from_bufread:

...

Now, additionally, because sudo caches credentials (usually for 5 minutes), you will notice that if you run the code with a correct password, but then run it again with the wrong one, the output of the directory will still be listed. The -k option is useful here because we can tell sudo to ignore the cached credentials:

From man 8 sudo, online version:

-k [command]

...

When used in conjunction with a command or an option that may require a password, the -k option will cause sudo to ignore the user's cached credentials. As a result, sudo will prompt for a password (if one is required by the security policy) and will not update the user's cached credentials.

So a useful bit would be adding -k to the args of the command:

fn sudo_cmd(pw: String) {
    let mut cmd = Command::new("sudo")
        .args(["-k", "-S", "ls"])
...

Now running with a wrong password after using a correct one fails, just like it should.

But there's a bit of a problem, it still tries to show output:

$ cargo run
Your password: # <- incorrect password entered here
Output:

Let's match against the status code of the command, 0 is usually taken to mean "everything went fine", with all the other codes really being specific to the command and based on vibes. If we look again in the man page for sudo:

Exit Value

Upon successful execution of a program, the exit status from sudo will simply be the exit status of the program that was executed.

Otherwise, sudo exits with a value of 1 if there is a configuration/permission problem or if sudo cannot execute the given command.

That's easy enough to check:

// inside sudo_cmd()
let output = cmd.wait_with_output().expect("wheres the output");

if output.status.success() {
    println!(
        "Output:\n{}",
        String::from_utf8(output.stdout).expect("Cant read stdout")
    );
} else {
    println!("Something went wrong :(");
}

Of course, ls has its return codes too:

Exit status:
0 if OK,
1 if minor problems (e.g., cannot access subdirectory),
2 if serious trouble (e.g., cannot access command-line argument).

So you can get creative with output.status.code() and check the text of stderr with String::from_utf8(output.stderr).contains("Permission denied") for instance (since, if you get a 1 exit code that could either mean sudo failed, or ls couldn't find the directory).

The final code looks like this:

use rpassword::prompt_password;
use std::{
    io::Write,
    process::{Command, Stdio},
    thread,
};

fn sudo_cmd(pw: String) {
    let mut cmd = Command::new("sudo")
        .args(["-k", "-S", "ls"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .expect("not spawned");

    let mut stdin = cmd.stdin.take().expect("Couldnt take stdin");

    thread::spawn(move || {
        stdin
            .write_all(pw.as_bytes())
            .expect("Couldnt write stding");
    });

    let output = cmd.wait_with_output().expect("wheres the output");

    if output.status.success() {
        println!(
            "Output:\n{}",
            String::from_utf8(output.stdout).expect("Cant read stdout")
        );
    } else {
        println!("Something went wrong :(");
    }
}

fn main() {
    let password = prompt_password("Your password: ").unwrap();
    sudo_cmd(password);
}

Now, concerning polkit, yes, it's better for an "actual" application since it throws up a dialog that can't be ignored. Interfacing with polkit is a bit...difficult, but for simple purposes like this, pkexec works fine for this—try running pkexec ls in a terminal, you should be greeted with something like this:

an Authentication Required dialog, thrown by pkexec, asking to execute ls

Modifying our program to use pkexec instead of sudo is easy, it's just a matter of changing the Command invocation and dropping our manual password input:

use std::process::{Command, Stdio};

fn polkit_cmd() {
    let cmd = Command::new("pkexec")
        .arg("ls")
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .expect("failed to spawn `pkexec`");

    let output = cmd.wait_with_output().expect("failed to wait on `pkexec`");

    if output.status.success() {
        println!(
            "Output:\n{}",
            String::from_utf8(output.stdout).expect("`pkexec` stdout isn't valid utf-8")
        );
    } else {
        println!("Something went wrong :(, did you not authenticate?");
    }
}

fn main() {
    polkit_cmd();
}

And of course you can match against the error codes for more granularity (127 is what pkexec fails with).

Not every computer might have polkit (and by extension pkexec), or even sudo, so you should do checks to make sure the user's computer has these before attempting to run them (command -v sudo is the best way to try this, wrap it in a Command and check the status). Just about everyone has su though, so that can be the lowest common denominator.

Upvotes: 2

Related Questions