Skip to content

Commit d473796

Browse files
committed
Add keyutils and secret_service attribute support.
1 parent 8912dd7 commit d473796

File tree

3 files changed

+154
-15
lines changed

3 files changed

+154
-15
lines changed

src/keyutils.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
33
# Linux kernel (keyutils) credential store
44
5-
Modern linux kernels have a built-in secure store, [keyutils](https://www.man7.org/linux/man-pages/man7/keyutils.7.html).
6-
This module (written primarily by [@landhb](https://github.com/landhb)) uses that secure store
7-
as the persistent back end for entries.
5+
Modern linux kernels have a built-in secure store,
6+
[keyutils](https://www.man7.org/linux/man-pages/man7/keyutils.7.html).
7+
This module (written primarily by [@landhb](https://github.com/landhb))
8+
uses that secure store as the persistent back end for entries.
89
910
Entries in keyutils are identified by a string `description`. If an entry is created with
1011
an explicit `target`, that value is used as the keyutils description. Otherwise, the string
1112
`keyring-rs:user@service` is used (where user and service come from the entry creation call).
1213
14+
There is no notion of attribute other than the description supported by keyutils,
15+
so the [get_attributes](Entry::get_attributes) and [update_attributes](Entry::update_attributes)
16+
calls are both no-ops for this credential store.
17+
1318
# Persistence
1419
1520
The key management facility provided by the kernel is completely in-memory and will not persist
@@ -403,6 +408,11 @@ mod tests {
403408
crate::tests::test_update(entry_new);
404409
}
405410

411+
#[test]
412+
fn test_noop_get_update_attributes() {
413+
crate::tests::test_noop_get_update_attributes(entry_new);
414+
}
415+
406416
#[test]
407417
fn test_get_credential() {
408418
let name = generate_random_string();

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ mod tests {
672672
}
673673
entry
674674
.delete_credential()
675-
.unwrap_or_else(|err| panic!("Can't delete password for attribute test: {err:?}"));
675+
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
676676
assert!(
677677
matches!(entry.get_attributes(), Err(Error::NoEntry)),
678678
"Read deleted credential in attribute test",

src/secret_service.rs

Lines changed: 140 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@
33
# secret-service credential store
44
55
Items in the secret-service are identified by an arbitrary collection
6-
of attributes, and each has "label" for use in graphical editors. This
7-
implementation uses the following attributes:
6+
of attributes. This implementation controls the following attributes:
87
98
- `target` (optional & taken from entry creation call, defaults to `default`)
109
- `service` (required & taken from entry creation call)
11-
- `username` (required & taken from entry creation call)
12-
- `application` (optional & always set to `rust-keyring`)
10+
- `username` (required & taken from entry creation call's `user` parameter)
11+
12+
In addition, when creating a new credential, this implementation assigns
13+
two additional attributes:
14+
15+
- `application` (set to `rust-keyring-client`)
16+
- `label` (set to a string with the user, service, and keyring version at time of creation)
17+
18+
Client code is allowed to retrieve and to set all attributes _except_ the
19+
three that are controlled by this implementation. (N.B. The `label` string
20+
is not actually an attribute; it's a required element in every item and is used
21+
by GUI tools as the name for the item. But this implementation treats the
22+
label as if it were any other non-controlled attribute, with the caveat that
23+
it will reject any attempt to set the label to an empty string.)
1324
1425
Existing items are always searched for at the service level, which
1526
means all collections are searched. The search attributes used are
@@ -20,11 +31,11 @@ that were stored in the default collection, a fallback search is done
2031
for items in the default collection with no `target` attribute *if
2132
the original search for all three attributes returns no matches*.
2233
23-
New items are always created with all three search attributes, and
24-
they are given a label that identifies the crate and version and
25-
attributes used in the entry. If a target other than `default` is
26-
specified for the entry, then a collection labeled with that target
27-
will be created (if necessary) to hold the new item.
34+
New items are created in the default collection,
35+
unless a target other than `default` is
36+
specified for the entry, in which case the item
37+
will be created in a collection (created if necessary)
38+
that is labeled with the specified target.
2839
2940
Setting the password on an entry will always update the password on an
3041
existing item in preference to creating a new item.
@@ -179,6 +190,23 @@ impl CredentialApi for SsCredential {
179190
Ok(secrets[0].clone())
180191
}
181192

193+
/// Get attributes on a unique matching item, if it exists
194+
///
195+
/// Same error conditions as [get_secret].
196+
fn get_attributes(&self) -> Result<HashMap<String, String>> {
197+
let attributes: Vec<HashMap<String, String>> =
198+
self.map_matching_items(get_item_attributes, true)?;
199+
Ok(attributes.into_iter().next().unwrap())
200+
}
201+
202+
/// Update attributes on a unique matching item, if it exists
203+
///
204+
/// Same error conditions as [get_secret].
205+
fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
206+
self.map_matching_items(|i| update_item_attributes(i, attributes), true)?;
207+
Ok(())
208+
}
209+
182210
/// Deletes the unique matching item, if it exists.
183211
///
184212
/// If there are no
@@ -227,7 +255,7 @@ impl SsCredential {
227255
Ok(Self {
228256
attributes,
229257
label: format!(
230-
"keyring-rs v{} for target '{target}', service '{service}', user '{user}'",
258+
"keyring v{}: {user}@{service}:{target}",
231259
env!("CARGO_PKG_VERSION"),
232260
),
233261
target: Some(target.to_string()),
@@ -496,12 +524,52 @@ pub fn get_item_password(item: &Item) -> Result<String> {
496524
decode_password(bytes)
497525
}
498526

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

533+
/// Given an existing item, retrieve its non-controlled attributes.
534+
pub fn get_item_attributes(item: &Item) -> Result<HashMap<String, String>> {
535+
let mut attributes = item.get_attributes().map_err(decode_error)?;
536+
attributes.remove("target");
537+
attributes.remove("service");
538+
attributes.remove("username");
539+
attributes.insert("label".to_string(), item.get_label().map_err(decode_error)?);
540+
Ok(attributes)
541+
}
542+
543+
/// Given an existing item, retrieve its non-controlled attributes.
544+
pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) -> Result<()> {
545+
let existing = item.get_attributes().map_err(decode_error)?;
546+
let mut updated: HashMap<&str, &str> = HashMap::new();
547+
for (k, v) in existing.iter() {
548+
updated.insert(k, v);
549+
}
550+
for (k, v) in attributes.iter() {
551+
if k.eq(&"target") || k.eq(&"service") || k.eq(&"username") {
552+
continue;
553+
}
554+
if k.eq(&"label") {
555+
if v.is_empty() {
556+
return Err(ErrorCode::Invalid(
557+
"label".to_string(),
558+
"cannot be empty".to_string(),
559+
));
560+
}
561+
item.set_label(v).map_err(decode_error)?;
562+
if updated.contains_key("label") {
563+
updated.insert("label", v);
564+
}
565+
} else {
566+
updated.insert(k, v);
567+
}
568+
}
569+
item.set_attributes(updated).map_err(decode_error)?;
570+
Ok(())
571+
}
572+
505573
// Given an existing item, delete it.
506574
pub fn delete_item(item: &Item) -> Result<()> {
507575
item.delete().map_err(decode_error)
@@ -542,6 +610,7 @@ fn wrap(err: Error) -> Box<dyn std::error::Error + Send + Sync> {
542610
mod tests {
543611
use crate::credential::CredentialPersistence;
544612
use crate::{tests::generate_random_string, Entry, Error};
613+
use std::collections::HashMap;
545614

546615
use super::{default_credential_builder, SsCredential};
547616

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

701+
#[test]
702+
fn test_get_update_attributes() {
703+
let name = generate_random_string();
704+
let credential = SsCredential::new_with_target(None, &name, &name)
705+
.expect("Can't create credential for attribute test");
706+
let create_label = credential.label.clone();
707+
let entry = Entry::new_with_credential(Box::new(credential));
708+
assert!(
709+
matches!(entry.get_attributes(), Err(Error::NoEntry)),
710+
"Read missing credential in attribute test",
711+
);
712+
let mut in_map: HashMap<&str, &str> = HashMap::new();
713+
in_map.insert("label", "test label value");
714+
in_map.insert("test attribute name", "test attribute value");
715+
in_map.insert("target", "ignored target value");
716+
in_map.insert("service", "ignored service value");
717+
in_map.insert("username", "ignored username value");
718+
assert!(
719+
matches!(entry.update_attributes(&in_map), Err(Error::NoEntry)),
720+
"Updated missing credential in attribute test",
721+
);
722+
// create the credential and test again
723+
entry
724+
.set_password("test password for attributes")
725+
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
726+
let out_map = entry
727+
.get_attributes()
728+
.expect("Can't get attributes after create");
729+
assert_eq!(out_map["label"], create_label);
730+
assert_eq!(out_map["application"], "rust-keyring");
731+
assert!(!out_map.contains_key("target"));
732+
assert!(!out_map.contains_key("service"));
733+
assert!(!out_map.contains_key("username"));
734+
assert!(
735+
matches!(entry.update_attributes(&in_map), Ok(())),
736+
"Couldn't update attributes in attribute test",
737+
);
738+
let after_map = entry
739+
.get_attributes()
740+
.expect("Can't get attributes after update");
741+
assert_eq!(after_map["label"], in_map["label"]);
742+
assert_eq!(
743+
after_map["test attribute name"],
744+
in_map["test attribute name"]
745+
);
746+
assert_eq!(out_map["application"], "rust-keyring");
747+
in_map.insert("label", "");
748+
assert!(
749+
matches!(entry.update_attributes(&in_map), Err(Error::Invalid(_, _))),
750+
"Was able to set empty label in attribute test",
751+
);
752+
entry
753+
.delete_credential()
754+
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
755+
assert!(
756+
matches!(entry.get_attributes(), Err(Error::NoEntry)),
757+
"Read deleted credential in attribute test",
758+
);
759+
}
760+
632761
#[test]
633762
#[ignore = "can't be run headless, because it needs to prompt"]
634763
fn test_create_new_target_collection() {

0 commit comments

Comments
 (0)