3
3
# secret-service credential store
4
4
5
5
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:
8
7
9
8
- `target` (optional & taken from entry creation call, defaults to `default`)
10
9
- `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.)
13
24
14
25
Existing items are always searched for at the service level, which
15
26
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
20
31
for items in the default collection with no `target` attribute *if
21
32
the original search for all three attributes returns no matches*.
22
33
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 .
28
39
29
40
Setting the password on an entry will always update the password on an
30
41
existing item in preference to creating a new item.
@@ -179,6 +190,23 @@ impl CredentialApi for SsCredential {
179
190
Ok ( secrets[ 0 ] . clone ( ) )
180
191
}
181
192
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
+
182
210
/// Deletes the unique matching item, if it exists.
183
211
///
184
212
/// If there are no
@@ -227,7 +255,7 @@ impl SsCredential {
227
255
Ok ( Self {
228
256
attributes,
229
257
label : format ! (
230
- "keyring-rs v{} for target '{target}', service ' {service}', user '{user}' " ,
258
+ "keyring v{}: {user}@ {service}:{target} " ,
231
259
env!( "CARGO_PKG_VERSION" ) ,
232
260
) ,
233
261
target : Some ( target. to_string ( ) ) ,
@@ -496,12 +524,52 @@ pub fn get_item_password(item: &Item) -> Result<String> {
496
524
decode_password ( bytes)
497
525
}
498
526
499
- //// Given an existing item, retrieve and decode its password .
527
+ //// Given an existing item, retrieve its secret .
500
528
pub fn get_item_secret ( item : & Item ) -> Result < Vec < u8 > > {
501
529
let secret = item. get_secret ( ) . map_err ( decode_error) ?;
502
530
Ok ( secret)
503
531
}
504
532
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
+
505
573
// Given an existing item, delete it.
506
574
pub fn delete_item ( item : & Item ) -> Result < ( ) > {
507
575
item. delete ( ) . map_err ( decode_error)
@@ -542,6 +610,7 @@ fn wrap(err: Error) -> Box<dyn std::error::Error + Send + Sync> {
542
610
mod tests {
543
611
use crate :: credential:: CredentialPersistence ;
544
612
use crate :: { tests:: generate_random_string, Entry , Error } ;
613
+ use std:: collections:: HashMap ;
545
614
546
615
use super :: { default_credential_builder, SsCredential } ;
547
616
@@ -629,6 +698,66 @@ mod tests {
629
698
assert ! ( matches!( entry. get_password( ) , Err ( Error :: NoEntry ) ) ) ;
630
699
}
631
700
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
+
632
761
#[ test]
633
762
#[ ignore = "can't be run headless, because it needs to prompt" ]
634
763
fn test_create_new_target_collection ( ) {
0 commit comments