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

Provide keyutils with persistence-after-reboot using secret-service #222

Merged
merged 20 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ jobs:
- "async-secret-service,async-io,crypto-rust"
- "async-secret-service,tokio,crypto-openssl"
- "async-secret-service,async-io,crypto-openssl"
- "linux-native-sync-persistent,crypto-rust"
- "linux-native-sync-persistent,crypto-openssl"
- "linux-native-async-persistent,tokio,crypto-rust"
- "linux-native-async-persistent,async-io,crypto-rust"
- "linux-native-async-persistent,tokio,crypto-openssl"
- "linux-native-async-persistent,async-io,crypto-openssl"

steps:
- name: Install CI dependencies
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ linux-native = ["dep:linux-keyutils"]
apple-native = ["dep:security-framework"]
windows-native = ["dep:windows-sys", "dep:byteorder"]

linux-native-sync-persistent = ["linux-native", "sync-secret-service"]
linux-native-async-persistent = ["linux-native", "async-secret-service"]
sync-secret-service = ["dep:dbus-secret-service"]
async-secret-service = ["dep:secret-service", "dep:zbus"]
crypto-rust = ["dbus-secret-service?/crypto-rust", "secret-service?/crypto-rust"]
Expand Down
250 changes: 250 additions & 0 deletions src/keyutils_persistent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/*!

# keyutils-persistent credential store

This store is a combination of the [keyutils](crate::keyutils) store
backed up with a persistent [secret-service](crate::secret_service)
store.

*/

use log::debug;

use super::credential::{
Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence,
};
use super::error::{Error, Result};
use super::keyutils::KeyutilsCredential;
use super::secret_service::SsCredential;

/// Representation of a keyutils-persistent credential.
///
/// The credential owns a [KeyutilsCredential] for in-memory usage and
/// a [SsCredential] for persistence.
#[derive(Debug, Clone)]
pub struct KeyutilsPersistentCredential {
keyutils: KeyutilsCredential,
ss: SsCredential,
}

impl CredentialApi for KeyutilsPersistentCredential {
/// Set a password in the underlying store
fn set_password(&self, password: &str) -> Result<()> {
self.set_secret(password.as_bytes())
}

/// Set a secret in the underlying store
///
/// It sets first the secret in keyutils, then in
/// secret-service. If the late one fails, keyutils secret change
/// is reverted.
fn set_secret(&self, secret: &[u8]) -> Result<()> {
let prev_secret = self.keyutils.get_secret();
self.keyutils.set_secret(secret)?;

if let Err(err) = self.ss.set_secret(secret) {
match prev_secret {
Ok(ref secret) => self.keyutils.set_secret(secret),
Err(Error::NoEntry) => self.keyutils.delete_credential(),
Err(err) => Err(err),
}?;

return Err(err);
}

Ok(())
}

/// Retrieve a password from the underlying store
///
/// The password is retrieved from keyutils. In case of error, the
/// password is retrieved from secret-service instead (and
/// keyutils is updated).
fn get_password(&self) -> Result<String> {
match self.keyutils.get_password() {
Ok(password) => {
return Ok(password);
}
Err(err) => {
debug!("cannot get password from keyutils: {err}, trying from secret service")
}
}

let password = self.ss.get_password().map_err(ambigous_to_no_entry)?;
self.keyutils.set_password(&password)?;
soywod marked this conversation as resolved.
Show resolved Hide resolved

Ok(password)
}

/// Retrieve a secret from the underlying store
///
/// The secret is retrieved from keyutils. In case of error, the
/// secret is retrieved from secret-service instead (and keyutils
/// is updated).
fn get_secret(&self) -> Result<Vec<u8>> {
match self.keyutils.get_secret() {
Ok(secret) => {
return Ok(secret);
}
Err(err) => {
debug!("cannot get secret from keyutils: {err}, trying from secret service")
}
}

let secret = self.ss.get_secret().map_err(ambigous_to_no_entry)?;
self.keyutils.set_secret(&secret)?;
soywod marked this conversation as resolved.
Show resolved Hide resolved

Ok(secret)
}

/// Delete a password from the underlying store.
///
/// The credential is deleted from both keyutils and
/// secret-service.
fn delete_credential(&self) -> Result<()> {
if let Err(err) = self.keyutils.delete_credential() {
debug!("cannot delete keyutils credential: {err}");
}

self.ss.delete_credential()
}

fn as_any(&self) -> &dyn std::any::Any {
self
}

fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}

impl KeyutilsPersistentCredential {
/// Create the platform credential for a Keyutils entry.
///
/// An explicit target string is interpreted as the KeyRing to use for the entry.
/// If none is provided, then we concatenate the user and service in the string
/// `keyring-rs:user@service`.
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
let ss = SsCredential::new_with_target(target, service, user)?;
let keyutils = KeyutilsCredential::new_with_target(target, service, user)?;
Ok(Self { keyutils, ss })
}
}
soywod marked this conversation as resolved.
Show resolved Hide resolved

/// The builder for keyutils-persistent credentials
#[derive(Debug, Default)]
pub struct KeyutilsPersistentCredentialBuilder {}

/// Returns an instance of the keyutils-persistent credential builder.
///
/// If keyutils-persistent is the default credential store, this is
/// called once when an entry is first created.
pub fn default_credential_builder() -> Box<CredentialBuilder> {
Box::new(KeyutilsPersistentCredentialBuilder {})
}

impl CredentialBuilderApi for KeyutilsPersistentCredentialBuilder {
/// Build an [KeyutilsPersistentCredential] for the given target, service, and user.
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
Ok(Box::new(SsCredential::new_with_target(
target, service, user,
)?))
}

/// Return the underlying builder object with an `Any` type so that it can
/// be downgraded to an [KeyutilsPersistentCredentialBuilder] for platform-specific processing.
fn as_any(&self) -> &dyn std::any::Any {
self
}

/// This keystore keeps credentials thanks to the inner secret-service store.
fn persistence(&self) -> CredentialPersistence {
CredentialPersistence::UntilDelete
}
}

/// Replace any Ambiguous error with a NoEntry one
fn ambigous_to_no_entry(err: Error) -> Error {
if let Error::Ambiguous(_) = err {
return Error::NoEntry;
};

err
}

#[cfg(test)]
mod tests {
use crate::credential::CredentialPersistence;
use crate::{Entry, Error};

use super::{default_credential_builder, KeyutilsPersistentCredential};

#[test]
fn test_persistence() {
assert!(matches!(
default_credential_builder().persistence(),
CredentialPersistence::UntilDelete
))
}

fn entry_new(service: &str, user: &str) -> Entry {
crate::tests::entry_from_constructor(
KeyutilsPersistentCredential::new_with_target,
service,
user,
)
}

#[test]
fn test_invalid_parameter() {
let credential = KeyutilsPersistentCredential::new_with_target(Some(""), "service", "user");
assert!(
matches!(credential, Err(Error::Invalid(_, _))),
"Created entry with empty target"
);
}

#[test]
fn test_empty_service_and_user() {
crate::tests::test_empty_service_and_user(entry_new);
}

#[test]
fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new);
}

#[test]
fn test_empty_password() {
let entry = entry_new("empty password service", "empty password user");
assert!(
matches!(entry.set_password(""), Err(Error::Invalid(_, _))),
"Able to set empty password"
);
}

#[test]
fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new);
}

#[test]
fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new);
}

#[test]
fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new);
}

#[test]
fn test_update() {
crate::tests::test_update(entry_new);
}

#[test]
fn test_noop_get_update_attributes() {
crate::tests::test_noop_get_update_attributes(entry_new);
}
}
51 changes: 37 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,48 +184,72 @@ pub mod mock;
//
// can't use both sync and async secret service
//
#[cfg(all(feature = "sync-secret-service", feature = "async-secret-service"))]
compile_error!("This crate cannot use the secret-service both synchronously and asynchronously");
#[cfg(any(
all(feature = "sync-secret-service", feature = "async-secret-service"),
all(
feature = "linux-native-sync-persistent",
feature = "linux-native-async-persistent",
)
))]
compile_error!("This crate cannot use both the sync and async versions of any credential store");

//
// pick the *nix keystore
//

#[cfg(all(target_os = "linux", feature = "linux-native"))]
pub mod keyutils;
// use keyutils as default if secret-service is not available
#[cfg(all(
target_os = "linux",
feature = "linux-native",
not(any(feature = "sync-secret-service", feature = "async-secret-service"))
not(feature = "sync-secret-service"),
not(feature = "async-secret-service"),
))]
soywod marked this conversation as resolved.
Show resolved Hide resolved
pub use keyutils as default;

#[cfg(all(
any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
any(feature = "sync-secret-service", feature = "async-secret-service")
any(feature = "sync-secret-service", feature = "async-secret-service"),
))]
pub mod secret_service;
// use secret-service as default if it's available
#[cfg(all(
any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
any(feature = "sync-secret-service", feature = "async-secret-service"),
not(any(
feature = "linux-native-sync-persistent",
feature = "linux-native-async-persistent",
)),
))]
pub use secret_service as default;

#[cfg(all(
target_os = "linux",
any(
feature = "linux-native-sync-persistent",
feature = "linux-native-async-persistent",
)
))]
pub mod keyutils_persistent;
#[cfg(all(
target_os = "linux",
any(
feature = "linux-native-sync-persistent",
feature = "linux-native-async-persistent",
),
))]
pub use keyutils_persistent as default;

// fallback to mock if neither keyutils nor secret service is available
#[cfg(any(
all(
target_os = "linux",
not(any(
feature = "linux-native",
feature = "sync-secret-service",
feature = "async-secret-service"
))
not(feature = "linux-native"),
not(feature = "sync-secret-service"),
not(feature = "async-secret-service"),
),
all(
any(target_os = "freebsd", target_os = "openbsd"),
not(any(feature = "sync-secret-service", feature = "async-secret-service"))
not(feature = "sync-secret-service"),
not(feature = "async-secret-service"),
)
))]
soywod marked this conversation as resolved.
Show resolved Hide resolved
pub use mock as default;
Expand All @@ -250,7 +274,6 @@ pub use mock as default;
//
// pick the Windows keystore
//

#[cfg(all(target_os = "windows", feature = "windows-native"))]
pub mod windows;
#[cfg(all(target_os = "windows", not(feature = "windows-native")))]
Expand Down