@@ -21,6 +21,11 @@ So if you have a custom algorithm you want to use for computing the Windows targ
2121you can specify the target name directly. (You still need to provide a service and username,
2222because they are used in the credential's metadata.)
2323
24+ The [get_attributes](Entry::get_attributes)
25+ call will return the values in the `username`, `comment`, and `target_alias` fields
26+ (using those strings as the attribute names), and the [update_attributes](Entry::update_attributes)
27+ call allows setting those fields.
28+
2429## Caveat
2530
2631Reads and writes of the same entry from multiple threads
@@ -31,6 +36,7 @@ different threads produces different results on different runs.
3136*/
3237
3338use byteorder:: { ByteOrder , LittleEndian } ;
39+ use std:: collections:: HashMap ;
3440use std:: iter:: once;
3541use std:: mem:: MaybeUninit ;
3642use std:: str;
@@ -89,47 +95,7 @@ impl CredentialApi for WinCredential {
8995 /// there is no chance of ambiguity.
9096 fn set_secret ( & self , secret : & [ u8 ] ) -> Result < ( ) > {
9197 self . validate_attributes ( Some ( secret) , None ) ?;
92- let mut username = to_wstr ( & self . username ) ;
93- let mut target_name = to_wstr ( & self . target_name ) ;
94- let mut target_alias = to_wstr ( & self . target_alias ) ;
95- let mut comment = to_wstr ( & self . comment ) ;
96- // Password strings are converted to UTF-16, because that's the native
97- // charset for Windows strings. This allows editing of the password in
98- // the Windows native UI. But the storage for the credential is actually
99- // a little-endian blob, because passwords can contain anything.
100- let mut blob = secret. to_vec ( ) ;
101- let blob_len = blob. len ( ) as u32 ;
102- let flags = CRED_FLAGS :: default ( ) ;
103- let cred_type = CRED_TYPE_GENERIC ;
104- let persist = CRED_PERSIST_ENTERPRISE ;
105- // Ignored by CredWriteW
106- let last_written = FILETIME {
107- dwLowDateTime : 0 ,
108- dwHighDateTime : 0 ,
109- } ;
110- let attribute_count = 0 ;
111- let attributes: * mut CREDENTIAL_ATTRIBUTEW = std:: ptr:: null_mut ( ) ;
112- let mut credential = CREDENTIALW {
113- Flags : flags,
114- Type : cred_type,
115- TargetName : target_name. as_mut_ptr ( ) ,
116- Comment : comment. as_mut_ptr ( ) ,
117- LastWritten : last_written,
118- CredentialBlobSize : blob_len,
119- CredentialBlob : blob. as_mut_ptr ( ) ,
120- Persist : persist,
121- AttributeCount : attribute_count,
122- Attributes : attributes,
123- TargetAlias : target_alias. as_mut_ptr ( ) ,
124- UserName : username. as_mut_ptr ( ) ,
125- } ;
126- // raw pointer to credential, is coerced from &mut
127- let p_credential: * const CREDENTIALW = & mut credential;
128- // Call windows API
129- match unsafe { CredWriteW ( p_credential, 0 ) } {
130- 0 => Err ( decode_error ( ) ) ,
131- _ => Ok ( ( ) ) ,
132- }
98+ self . save_credential ( secret)
13399 }
134100
135101 /// Look up the password for this entry, if any.
@@ -148,6 +114,39 @@ impl CredentialApi for WinCredential {
148114 self . extract_from_platform ( extract_secret)
149115 }
150116
117+ /// Get the attributes from the credential for this entry, if it exists.
118+ ///
119+ /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
120+ /// credential in the store.
121+ fn get_attributes ( & self ) -> Result < HashMap < String , String > > {
122+ let cred = self . extract_from_platform ( Self :: extract_credential) ?;
123+ let mut attributes: HashMap < String , String > = HashMap :: new ( ) ;
124+ attributes. insert ( "comment" . to_string ( ) , cred. comment . clone ( ) ) ;
125+ attributes. insert ( "target_alias" . to_string ( ) , cred. target_alias . clone ( ) ) ;
126+ attributes. insert ( "username" . to_string ( ) , cred. username . clone ( ) ) ;
127+ Ok ( attributes)
128+ }
129+
130+ /// Update the attributes on the credential for this entry, if it exists.
131+ ///
132+ /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
133+ /// credential in the store.
134+ fn update_attributes ( & self , attributes : & HashMap < & str , & str > ) -> Result < ( ) > {
135+ let secret = self . extract_from_platform ( extract_secret) ?;
136+ let mut cred = self . extract_from_platform ( Self :: extract_credential) ?;
137+ if let Some ( comment) = attributes. get ( & "comment" ) {
138+ cred. comment = comment. to_string ( ) ;
139+ }
140+ if let Some ( target_alias) = attributes. get ( & "target_alias" ) {
141+ cred. target_alias = target_alias. to_string ( ) ;
142+ }
143+ if let Some ( username) = attributes. get ( & "username" ) {
144+ cred. username = username. to_string ( ) ;
145+ }
146+ cred. validate_attributes ( Some ( & secret) , None ) ?;
147+ cred. save_credential ( & secret)
148+ }
149+
151150 /// Delete the underlying generic credential for this entry, if any.
152151 ///
153152 /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
@@ -227,6 +226,49 @@ impl WinCredential {
227226 Ok ( ( ) )
228227 }
229228
229+ /// Write this credential into the underlying store as a Generic credential
230+ ///
231+ /// You must always have validated attributes before you call this!
232+ fn save_credential ( & self , secret : & [ u8 ] ) -> Result < ( ) > {
233+ let mut username = to_wstr ( & self . username ) ;
234+ let mut target_name = to_wstr ( & self . target_name ) ;
235+ let mut target_alias = to_wstr ( & self . target_alias ) ;
236+ let mut comment = to_wstr ( & self . comment ) ;
237+ let mut blob = secret. to_vec ( ) ;
238+ let blob_len = blob. len ( ) as u32 ;
239+ let flags = CRED_FLAGS :: default ( ) ;
240+ let cred_type = CRED_TYPE_GENERIC ;
241+ let persist = CRED_PERSIST_ENTERPRISE ;
242+ // Ignored by CredWriteW
243+ let last_written = FILETIME {
244+ dwLowDateTime : 0 ,
245+ dwHighDateTime : 0 ,
246+ } ;
247+ let attribute_count = 0 ;
248+ let attributes: * mut CREDENTIAL_ATTRIBUTEW = std:: ptr:: null_mut ( ) ;
249+ let mut credential = CREDENTIALW {
250+ Flags : flags,
251+ Type : cred_type,
252+ TargetName : target_name. as_mut_ptr ( ) ,
253+ Comment : comment. as_mut_ptr ( ) ,
254+ LastWritten : last_written,
255+ CredentialBlobSize : blob_len,
256+ CredentialBlob : blob. as_mut_ptr ( ) ,
257+ Persist : persist,
258+ AttributeCount : attribute_count,
259+ Attributes : attributes,
260+ TargetAlias : target_alias. as_mut_ptr ( ) ,
261+ UserName : username. as_mut_ptr ( ) ,
262+ } ;
263+ // raw pointer to credential, is coerced from &mut
264+ let p_credential: * const CREDENTIALW = & mut credential;
265+ // Call windows API
266+ match unsafe { CredWriteW ( p_credential, 0 ) } {
267+ 0 => Err ( decode_error ( ) ) ,
268+ _ => Ok ( ( ) ) ,
269+ }
270+ }
271+
230272 /// Construct a credential from this credential's underlying Generic credential.
231273 ///
232274 /// This can be useful for seeing modifications made by a third party.
@@ -624,6 +666,57 @@ mod tests {
624666 crate :: tests:: test_update ( entry_new) ;
625667 }
626668
669+ #[ test]
670+ fn test_get_update_attributes ( ) {
671+ let name = generate_random_string ( ) ;
672+ let cred = WinCredential :: new_with_target ( None , & name, & name)
673+ . expect ( "Can't create credential for attribute test" ) ;
674+ let entry = Entry :: new_with_credential ( Box :: new ( cred. clone ( ) ) ) ;
675+ assert ! (
676+ matches!( entry. get_attributes( ) , Err ( ErrorCode :: NoEntry ) ) ,
677+ "Read missing credential in attribute test" ,
678+ ) ;
679+ let mut in_map: HashMap < & str , & str > = HashMap :: new ( ) ;
680+ in_map. insert ( "label" , "ignored label value" ) ;
681+ in_map. insert ( "attribute name" , "ignored attribute value" ) ;
682+ in_map. insert ( "target_alias" , "target alias value" ) ;
683+ in_map. insert ( "comment" , "comment value" ) ;
684+ in_map. insert ( "username" , "username value" ) ;
685+ assert ! (
686+ matches!( entry. update_attributes( & in_map) , Err ( ErrorCode :: NoEntry ) ) ,
687+ "Updated missing credential in attribute test" ,
688+ ) ;
689+ // create the credential and test again
690+ entry
691+ . set_password ( "test password for attributes" )
692+ . unwrap_or_else ( |err| panic ! ( "Can't set password for attribute test: {err:?}" ) ) ;
693+ let out_map = entry
694+ . get_attributes ( )
695+ . expect ( "Can't get attributes after create" ) ;
696+ assert_eq ! ( out_map[ "target_alias" ] , cred. target_alias) ;
697+ assert_eq ! ( out_map[ "comment" ] , cred. comment) ;
698+ assert_eq ! ( out_map[ "username" ] , cred. username) ;
699+ assert ! (
700+ matches!( entry. update_attributes( & in_map) , Ok ( ( ) ) ) ,
701+ "Couldn't update attributes in attribute test" ,
702+ ) ;
703+ let after_map = entry
704+ . get_attributes ( )
705+ . expect ( "Can't get attributes after update" ) ;
706+ assert_eq ! ( after_map[ "target_alias" ] , in_map[ "target_alias" ] ) ;
707+ assert_eq ! ( after_map[ "comment" ] , in_map[ "comment" ] ) ;
708+ assert_eq ! ( after_map[ "username" ] , in_map[ "username" ] ) ;
709+ assert ! ( !after_map. contains_key( "label" ) ) ;
710+ assert ! ( !after_map. contains_key( "attribute name" ) ) ;
711+ entry
712+ . delete_credential ( )
713+ . unwrap_or_else ( |err| panic ! ( "Can't delete credential for attribute test: {err:?}" ) ) ;
714+ assert ! (
715+ matches!( entry. get_attributes( ) , Err ( ErrorCode :: NoEntry ) ) ,
716+ "Read deleted credential in attribute test" ,
717+ ) ;
718+ }
719+
627720 #[ test]
628721 fn test_get_credential ( ) {
629722 let name = generate_random_string ( ) ;
0 commit comments