Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Find what linux-keystore is supported #123

Closed
wolfv opened this issue May 15, 2023 · 5 comments
Closed

Find what linux-keystore is supported #123

wolfv opened this issue May 15, 2023 · 5 comments
Assignees
Labels

Comments

@wolfv
Copy link

wolfv commented May 15, 2023

We're working on an application (package manager) that needs to run in headless and non-headless mode.

I would like to set a default storage on Linux based on wether the gnome-keyring / libsecret package is available or not. Is there a function to figure this out dynamically (at runtime)?

I am considering to implement my own storage provider as a fallback that writes the credentials to a JSON file in the home folder when no gnome-keyring is available. I think it would be nice if there was a good way to provide a fallback provider. Any thoughts on that?

Thanks for the nice library by the way!

@brotskydotcom
Copy link
Collaborator

Hi @wolfv thanks for the thanks :)!

If you are running on Linux then it seems you could always use the Linux kernel's keyutils storage module (which this module supports). It runs fine both headless and non-headless, and it's included in all modern Linux systems. That would save you the trouble both of trying to detect the existence of the gnome keyring and writing your own fallback provider. Depending on their usage scenario, your users might want to make the persistence of the entries longer than the default 3-5 days between usage (which would require admin access). But it would be easy for you to write a command-line utility that made that adjustment for them (which they would run with sudo).

Is there a particular reason you would like to switch to gnome-keyring on headed systems? Using gnome-keyring would have the following drawbacks:

  • when a user accesses their system remotely via ssh, they would not be able to access any of their stored credentials;
  • gnome-keyring uses async interfaces under the hood, so your build would include an entire Rust async subsystem (which is many megabytes large). That would also force the hand of any of your users that might want to use a different async subsystem (if they are working in Rust), unless you were allowing them to rebuild your package system with different options.

I hope this is useful information for you. Please continue the discussion if you have more questions.

@wolfv
Copy link
Author

wolfv commented May 16, 2023

That's good to know. Our package manager should run nicely in both CI settings and interactive "desktop" workflows. I have the feeling that the gnome-keyring is better integrated in an interactive desktop setting, and we'd want the credentials to stay there indefinitely.

Are there any tips on detecting the availability of gnome-keyring? Would getting a random entry and checking the error work?

We've also experimented a bit with implementing a credentials daemon (git style, https://git-scm.com/docs/git-credential-cache). We could try to upstream that if it's interesting, but falling back to teh built in linux keyutils store seems like another good option.

@brotskydotcom
Copy link
Collaborator

brotskydotcom commented May 18, 2023

Thanks for the update - it helps me understand your goals better.

I'm not a super linux expert, but @landhb is so I'm tagging him here to take a look at your question.

My take: I'm not really sure what happens if you run a binary that calls the secret service on a system that isn't running the secret service. As long as you can assume your headless CI boxes are all running the secret service, then yes I would imagine that the simplest way to detect whether you're headless would be simply to try and use a credential and look at whether you get an access denied error. But I'm not sure whether that would be distinguishable from the error you get when a headed user hadn't unlocked the keyring yet. So it might almost be easier to just have your users declare in their invocation whether they are running headed or not. That would always allow you to use the right store for that situation.

A completely different approach, by the way, might be to always use the kernel keystore and write a separate tool that headed users would use to transfer their gnome credentials into the persistent store before a run. (You could use keyring to write that separate tool.) That way your CI system would completely avoid all the bloat that comes with supporting the secret service and not bias your async rust users in any way.

@landhb
Copy link
Contributor

landhb commented May 18, 2023

For my personal project, I use the keyutils backend exclusively and re-prompt the user for their password if the entry isn't in memory. That password is used to decrypt a state file on disk, but the password itself is never stored on disk since there would be no secure way to do so. This is functionally the same as unlocking the secret-service keyring with a password when you first login.

However you can create a builder that tries to use secret-service first, and then falls back to keyutils on systems where it doesn't exist. For example:

use keyring::{
    credential::{CredentialApi, CredentialBuilderApi},
    keyutils::KeyutilsCredential,
    secret_service::SsCredential,
    Entry,
};
use std::any::Any;
use std::error::Error;
use dialoguer::Password;

/// A custom builder that falls back to keyutils
#[derive(Debug)]
struct FallbackBuilder {
    /// Indicator to only test once
    secret_service_missing: bool,
}

impl FallbackBuilder {
    fn new() -> Result<Self, Box<dyn Error>> {
        // Create a fake cred
        let ss = SsCredential::new_with_target(None, "test", "user")?;

        // Force a connect to secret service to determine if it exists
        let missing = match ss.map_matching_items(|_item| Ok(()), false) {
            Err(keyring::Error::PlatformFailure(_x)) => true,
            _ => false,
        };
        Ok(Self {
            secret_service_missing: missing,
        })
    }
}

impl CredentialBuilderApi for FallbackBuilder {
    /// Helper method to try secret-service first, then fallback to the kernel's store
    fn build(
        &self,
        target: Option<&str>,
        service: &str,
        user: &str,
    ) -> Result<Box<(dyn CredentialApi + Send + Sync + 'static)>, keyring::Error> {
        // First try secret-service if it exists
        if !self.secret_service_missing {
            let cred = SsCredential::new_with_target(target, service, user)?;
            return Ok(Box::new(cred));
        }

        // Fallback to the kernel's keystore
        let cred = KeyutilsCredential::new_with_target(target, service, user)?;
        Ok(Box::new(cred))
    }
    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    // Replace the default store at startup
    let backend = FallbackBuilder::new()?;
    keyring::set_default_credential_builder(Box::new(backend));

    // Start the entry
    let entry = Entry::new("test", "user")?;

    // Attempt to get the entry, if it doesn't exist, prompt the user
    // instead.
    let _password = match entry.get_password() {
    	Ok(pass) => pass,
    	_ => {
            // Prompt the user
            let passphrase = Password::new().with_prompt("Re-enter password").interact()?;

            // Store the password in the available store
            entry.set_password(&passphrase)?;

            // Use this for now
            passphrase
    	}
    };
    Ok(())
}

@brotskydotcom
Copy link
Collaborator

@wolfv Have you got more questions about this? If not, I'll close this issue in a few days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants