use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
use super::error::{decode_password, Error as ErrorCode, Result};
use linux_keyutils::{KeyError, KeyRing, KeyRingIdentifier};
#[derive(Debug, Clone)]
pub struct KeyutilsCredential {
pub session: KeyRing,
pub persistent: KeyRing,
pub description: String,
}
impl CredentialApi for KeyutilsCredential {
fn set_password(&self, password: &str) -> Result<()> {
if password.is_empty() {
return Err(ErrorCode::Invalid(
"password".to_string(),
"cannot be empty".to_string(),
));
}
let key = self
.session
.add_key(&self.description, password)
.map_err(decode_error)?;
self.persistent.link_key(key).map_err(decode_error)?;
Ok(())
}
fn get_password(&self) -> Result<String> {
let key = self
.session
.search(&self.description)
.map_err(decode_error)?;
self.session.link_key(key).map_err(decode_error)?;
self.persistent.link_key(key).map_err(decode_error)?;
let buffer = key.read_to_vec().map_err(decode_error)?;
decode_password(buffer)
}
fn delete_password(&self) -> Result<()> {
let key = self
.session
.search(&self.description)
.map_err(decode_error)?;
key.invalidate().map_err(decode_error)?;
Ok(())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl KeyutilsCredential {
pub fn get_credential(&self) -> Result<Self> {
self.session
.search(&self.description)
.map_err(decode_error)?;
Ok(self.clone())
}
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
let session =
KeyRing::from_special_id(KeyRingIdentifier::Session, false).map_err(decode_error)?;
let persistent =
KeyRing::get_persistent(KeyRingIdentifier::Session).map_err(decode_error)?;
let description = match target {
Some(value) if value.is_empty() => {
return Err(ErrorCode::Invalid(
"target".to_string(),
"cannot be empty".to_string(),
));
}
Some(value) => value.to_string(),
None => format!("keyring-rs:{user}@{service}"),
};
Ok(Self {
session,
persistent,
description,
})
}
}
#[derive(Debug, Copy, Clone)]
struct KeyutilsCredentialBuilder {}
pub fn default_credential_builder() -> Box<CredentialBuilder> {
Box::new(KeyutilsCredentialBuilder {})
}
impl CredentialBuilderApi for KeyutilsCredentialBuilder {
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
Ok(Box::new(KeyutilsCredential::new_with_target(
target, service, user,
)?))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
fn decode_error(err: KeyError) -> ErrorCode {
match err {
KeyError::KeyDoesNotExist
| KeyError::AccessDenied
| KeyError::KeyRevoked
| KeyError::KeyExpired => ErrorCode::NoEntry,
KeyError::InvalidDescription => ErrorCode::Invalid(
"description".to_string(),
"rejected by platform".to_string(),
),
KeyError::InvalidArguments => {
ErrorCode::Invalid("password".to_string(), "rejected by platform".to_string())
}
other => ErrorCode::PlatformFailure(wrap(other)),
}
}
fn wrap(err: KeyError) -> Box<dyn std::error::Error + Send + Sync> {
Box::new(err)
}
#[cfg(test)]
mod tests {
use crate::{tests::generate_random_string, Entry, Error};
use super::KeyutilsCredential;
fn entry_new(service: &str, user: &str) -> Entry {
crate::tests::entry_from_constructor(KeyutilsCredential::new_with_target, service, user)
}
#[test]
fn test_invalid_parameter() {
let credential = KeyutilsCredential::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_update() {
crate::tests::test_update(entry_new);
}
#[test]
fn test_get_credential() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let credential: &KeyutilsCredential = entry
.get_credential()
.downcast_ref()
.expect("Not a Keyutils credential");
assert!(
credential.get_credential().is_err(),
"Platform credential shouldn't exist yet!"
);
entry
.set_password("test get_credential")
.expect("Can't set password for get_credential");
assert!(credential.get_credential().is_ok());
entry
.delete_password()
.expect("Couldn't delete after get_credential");
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
}
}