Skip to content

Commit

Permalink
Add keyutils and secret_service attribute support.
Browse files Browse the repository at this point in the history
  • Loading branch information
brotskydotcom committed Sep 16, 2024
1 parent 8912dd7 commit d473796
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 15 deletions.
16 changes: 13 additions & 3 deletions src/keyutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
# Linux kernel (keyutils) credential store
Modern linux kernels have a built-in secure store, [keyutils](https://www.man7.org/linux/man-pages/man7/keyutils.7.html).
This module (written primarily by [@landhb](https://github.com/landhb)) uses that secure store
as the persistent back end for entries.
Modern linux kernels have a built-in secure store,
[keyutils](https://www.man7.org/linux/man-pages/man7/keyutils.7.html).
This module (written primarily by [@landhb](https://github.com/landhb))
uses that secure store as the persistent back end for entries.
Entries in keyutils are identified by a string `description`. If an entry is created with
an explicit `target`, that value is used as the keyutils description. Otherwise, the string
`keyring-rs:user@service` is used (where user and service come from the entry creation call).
There is no notion of attribute other than the description supported by keyutils,
so the [get_attributes](Entry::get_attributes) and [update_attributes](Entry::update_attributes)
calls are both no-ops for this credential store.
# Persistence
The key management facility provided by the kernel is completely in-memory and will not persist
Expand Down Expand Up @@ -403,6 +408,11 @@ mod tests {
crate::tests::test_update(entry_new);
}

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

#[test]
fn test_get_credential() {
let name = generate_random_string();
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ mod tests {
}
entry
.delete_credential()
.unwrap_or_else(|err| panic!("Can't delete password for attribute test: {err:?}"));
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
"Read deleted credential in attribute test",
Expand Down
151 changes: 140 additions & 11 deletions src/secret_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@
# secret-service credential store
Items in the secret-service are identified by an arbitrary collection
of attributes, and each has "label" for use in graphical editors. This
implementation uses the following attributes:
of attributes. This implementation controls the following attributes:
- `target` (optional & taken from entry creation call, defaults to `default`)
- `service` (required & taken from entry creation call)
- `username` (required & taken from entry creation call)
- `application` (optional & always set to `rust-keyring`)
- `username` (required & taken from entry creation call's `user` parameter)
In addition, when creating a new credential, this implementation assigns
two additional attributes:
- `application` (set to `rust-keyring-client`)
- `label` (set to a string with the user, service, and keyring version at time of creation)
Client code is allowed to retrieve and to set all attributes _except_ the
three that are controlled by this implementation. (N.B. The `label` string
is not actually an attribute; it's a required element in every item and is used
by GUI tools as the name for the item. But this implementation treats the
label as if it were any other non-controlled attribute, with the caveat that
it will reject any attempt to set the label to an empty string.)
Existing items are always searched for at the service level, which
means all collections are searched. The search attributes used are
Expand All @@ -20,11 +31,11 @@ that were stored in the default collection, a fallback search is done
for items in the default collection with no `target` attribute *if
the original search for all three attributes returns no matches*.
New items are always created with all three search attributes, and
they are given a label that identifies the crate and version and
attributes used in the entry. If a target other than `default` is
specified for the entry, then a collection labeled with that target
will be created (if necessary) to hold the new item.
New items are created in the default collection,
unless a target other than `default` is
specified for the entry, in which case the item
will be created in a collection (created if necessary)
that is labeled with the specified target.
Setting the password on an entry will always update the password on an
existing item in preference to creating a new item.
Expand Down Expand Up @@ -179,6 +190,23 @@ impl CredentialApi for SsCredential {
Ok(secrets[0].clone())
}

/// Get attributes on a unique matching item, if it exists
///
/// Same error conditions as [get_secret].
fn get_attributes(&self) -> Result<HashMap<String, String>> {
let attributes: Vec<HashMap<String, String>> =
self.map_matching_items(get_item_attributes, true)?;
Ok(attributes.into_iter().next().unwrap())
}

/// Update attributes on a unique matching item, if it exists
///
/// Same error conditions as [get_secret].
fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
self.map_matching_items(|i| update_item_attributes(i, attributes), true)?;
Ok(())
}

/// Deletes the unique matching item, if it exists.
///
/// If there are no
Expand Down Expand Up @@ -227,7 +255,7 @@ impl SsCredential {
Ok(Self {
attributes,
label: format!(
"keyring-rs v{} for target '{target}', service '{service}', user '{user}'",
"keyring v{}: {user}@{service}:{target}",
env!("CARGO_PKG_VERSION"),
),
target: Some(target.to_string()),
Expand Down Expand Up @@ -496,12 +524,52 @@ pub fn get_item_password(item: &Item) -> Result<String> {
decode_password(bytes)
}

//// Given an existing item, retrieve and decode its password.
//// Given an existing item, retrieve its secret.
pub fn get_item_secret(item: &Item) -> Result<Vec<u8>> {
let secret = item.get_secret().map_err(decode_error)?;
Ok(secret)
}

/// Given an existing item, retrieve its non-controlled attributes.
pub fn get_item_attributes(item: &Item) -> Result<HashMap<String, String>> {
let mut attributes = item.get_attributes().map_err(decode_error)?;
attributes.remove("target");
attributes.remove("service");
attributes.remove("username");
attributes.insert("label".to_string(), item.get_label().map_err(decode_error)?);
Ok(attributes)
}

/// Given an existing item, retrieve its non-controlled attributes.
pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) -> Result<()> {
let existing = item.get_attributes().map_err(decode_error)?;
let mut updated: HashMap<&str, &str> = HashMap::new();
for (k, v) in existing.iter() {
updated.insert(k, v);
}
for (k, v) in attributes.iter() {
if k.eq(&"target") || k.eq(&"service") || k.eq(&"username") {
continue;
}
if k.eq(&"label") {
if v.is_empty() {
return Err(ErrorCode::Invalid(
"label".to_string(),
"cannot be empty".to_string(),
));
}
item.set_label(v).map_err(decode_error)?;
if updated.contains_key("label") {
updated.insert("label", v);
}
} else {
updated.insert(k, v);
}
}
item.set_attributes(updated).map_err(decode_error)?;
Ok(())
}

// Given an existing item, delete it.
pub fn delete_item(item: &Item) -> Result<()> {
item.delete().map_err(decode_error)
Expand Down Expand Up @@ -542,6 +610,7 @@ fn wrap(err: Error) -> Box<dyn std::error::Error + Send + Sync> {
mod tests {
use crate::credential::CredentialPersistence;
use crate::{tests::generate_random_string, Entry, Error};
use std::collections::HashMap;

use super::{default_credential_builder, SsCredential};

Expand Down Expand Up @@ -629,6 +698,66 @@ mod tests {
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
}

#[test]
fn test_get_update_attributes() {
let name = generate_random_string();
let credential = SsCredential::new_with_target(None, &name, &name)
.expect("Can't create credential for attribute test");
let create_label = credential.label.clone();
let entry = Entry::new_with_credential(Box::new(credential));
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
"Read missing credential in attribute test",
);
let mut in_map: HashMap<&str, &str> = HashMap::new();
in_map.insert("label", "test label value");
in_map.insert("test attribute name", "test attribute value");
in_map.insert("target", "ignored target value");
in_map.insert("service", "ignored service value");
in_map.insert("username", "ignored username value");
assert!(
matches!(entry.update_attributes(&in_map), Err(Error::NoEntry)),
"Updated missing credential in attribute test",
);
// create the credential and test again
entry
.set_password("test password for attributes")
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
let out_map = entry
.get_attributes()
.expect("Can't get attributes after create");
assert_eq!(out_map["label"], create_label);
assert_eq!(out_map["application"], "rust-keyring");
assert!(!out_map.contains_key("target"));
assert!(!out_map.contains_key("service"));
assert!(!out_map.contains_key("username"));
assert!(
matches!(entry.update_attributes(&in_map), Ok(())),
"Couldn't update attributes in attribute test",
);
let after_map = entry
.get_attributes()
.expect("Can't get attributes after update");
assert_eq!(after_map["label"], in_map["label"]);
assert_eq!(
after_map["test attribute name"],
in_map["test attribute name"]
);
assert_eq!(out_map["application"], "rust-keyring");
in_map.insert("label", "");
assert!(
matches!(entry.update_attributes(&in_map), Err(Error::Invalid(_, _))),
"Was able to set empty label in attribute test",
);
entry
.delete_credential()
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
"Read deleted credential in attribute test",
);
}

#[test]
#[ignore = "can't be run headless, because it needs to prompt"]
fn test_create_new_target_collection() {
Expand Down

0 comments on commit d473796

Please sign in to comment.