Skip to content

Commit 694cb8c

Browse files
committed
Add attribute support to Windows.
1 parent d473796 commit 694cb8c

File tree

1 file changed

+134
-41
lines changed

1 file changed

+134
-41
lines changed

src/windows.rs

Lines changed: 134 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ So if you have a custom algorithm you want to use for computing the Windows targ
2121
you can specify the target name directly. (You still need to provide a service and username,
2222
because 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
2631
Reads and writes of the same entry from multiple threads
@@ -31,6 +36,7 @@ different threads produces different results on different runs.
3136
*/
3237

3338
use byteorder::{ByteOrder, LittleEndian};
39+
use std::collections::HashMap;
3440
use std::iter::once;
3541
use std::mem::MaybeUninit;
3642
use 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

Comments
 (0)