From 747364b6d0f938c15cce6e8d1de06e2890c48c73 Mon Sep 17 00:00:00 2001 From: wiimmers <91842532+wiimmers@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:56:52 -0400 Subject: [PATCH 01/14] Windows search feature --- Cargo.toml | 1 + src/credential.rs | 21 ++++++- src/error.rs | 5 ++ src/lib.rs | 91 +++++++++++++++++++++++++++- src/windows.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 261 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7719864..be35f35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ windows-test-threading = [] [dependencies] lazy_static = "1" +regex = "1.10.4" [target.'cfg(target_os = "macos")'.dependencies] security-framework = { version = "2.6", optional = true } diff --git a/src/credential.rs b/src/credential.rs index 632ddc5..d35ca9a 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -9,7 +9,7 @@ in a thread-safe way, a requirement captured in the [CredentialBuilder] and [CredentialApi] types that wrap them. */ use super::Result; -use std::any::Any; +use std::{collections::HashMap, any::Any}; /// The API that [credentials](Credential) implement. pub trait CredentialApi { @@ -91,3 +91,22 @@ impl std::fmt::Debug for CredentialBuilder { /// A thread-safe implementation of the [CredentialBuilder API](CredentialBuilderApi). pub type CredentialBuilder = dyn CredentialBuilderApi + Send + Sync; + +pub trait CredentialSearchApi { + fn by(&self, by: &str, query: &str) -> Result>>; +} + +pub type CredentialSearch = dyn CredentialSearchApi + Send + Sync; + +pub type CredentialSearchResult = Result>>; + +pub trait CredentialListApi { + fn list_credentials(search_result: Result>>, limit: Limit) -> Result<()>; +} + +pub type CredentialList = dyn CredentialListApi + Send + Sync; + +pub enum Limit { + All, + Max(i64) +} diff --git a/src/error.rs b/src/error.rs index 7b20917..e0f7a3a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -54,6 +54,8 @@ pub enum Error { /// This indicates that there is more than one credential found in the store /// that matches the entry. Its value is a vector of the matching credentials. Ambiguous(Vec>), + + SearchError(String), } pub type Result = std::result::Result; @@ -81,6 +83,9 @@ impl std::fmt::Display for Error { items.len(), ) } + Error::SearchError(reason) => { + write!(f, "Error searching for credential: {}", reason) + } } } } diff --git a/src/lib.rs b/src/lib.rs index 292001e..0fac21a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,9 +113,8 @@ entry creation doesn't go through the platform credential manager. It's fine to create an entry on one thread and then immediately use it on a different thread. This is thoroughly tested on all platforms.) */ -pub use credential::{Credential, CredentialBuilder}; +pub use credential::{Credential, CredentialBuilder, CredentialSearch, CredentialSearchResult, CredentialList, Limit}; pub use error::{Error, Result}; - // Included keystore implementations and default choice thereof. pub mod mock; @@ -307,6 +306,94 @@ impl Entry { } } +fn default_credential_search() -> Result { + let credentials = default::default_credential_search(); + Ok(Search {inner: credentials}) +} + + +pub struct Search { + inner: Box +} + +impl Search { + /// Create a new instance of the Credential Search. + /// + /// The default credential search is used. + pub fn new() -> Result { + default_credential_search() + } + /// Specifies what parameter to search by and the query string + /// + /// Can return a [SearchError](Error::SearchError) + /// # Example + /// let search = keyring::Search::new().unwrap(); + /// let results = search.by("user", "Mr. Foo Bar"); + pub fn by(&self, by: &str, query: &str) -> CredentialSearchResult { + self.inner.by(by, query) + } +} + +pub struct List {} + +impl List { + pub fn list_credentials(search_result: CredentialSearchResult, limit: Limit) -> Result<()> { + match limit { + Limit::All => { + match Self::list_all(search_result) { + Ok(_) => Ok(()), + Err(err) => return Err(Error::SearchError(err.to_string())) + } + }, + Limit::Max(max) => { + match Self::list_max(search_result, max) { + Ok(_) => Ok(()), + Err(err) => return Err(Error::SearchError(err.to_string())) + } + + } + } + + + } + + fn list_all(result: CredentialSearchResult) -> Result<()> { + match result { + Ok(search_result) => { + for (outer_key, inner_map) in search_result { + println!("{outer_key}"); + for (key, value) in inner_map { + println!("{key} {value}"); + } + } + Ok(()) + }, + Err(err) => return Err(Error::SearchError(err.to_string())) + } + } + + fn list_max(result: CredentialSearchResult, max: i64) -> Result<()> { + + match result { + Ok(search_result) => { + let mut count = 1; + for (outer_key, inner_map) in search_result { + println!("{outer_key}"); + for (key, value) in inner_map { + println!("{key} {value}"); + } + count += 1; + if count > max { + break; + } + } + Ok(()) + }, + Err(err) => return Err(Error::SearchError(err.to_string())) + } + } +} + #[cfg(doctest)] doc_comment::doctest!("../README.md", readme); diff --git a/src/windows.rs b/src/windows.rs index 99efca7..d4619ed 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -30,6 +30,8 @@ test suite of this crate, and they have been observed to fail in both Windows 10 and Windows 11. */ use byteorder::{ByteOrder, LittleEndian}; +pub use regex::Regex; +use std::collections::HashMap; use std::iter::once; use std::mem::MaybeUninit; use std::str; @@ -38,12 +40,13 @@ use windows_sys::Win32::Foundation::{ ERROR_NOT_FOUND, ERROR_NO_SUCH_LOGON_SESSION, FILETIME, }; use windows_sys::Win32::Security::Credentials::{ - CredDeleteW, CredFree, CredReadW, CredWriteW, CREDENTIALW, CREDENTIAL_ATTRIBUTEW, CRED_FLAGS, + CredDeleteW, CredEnumerateW, CredFree, CredReadW, CredWriteW, + CREDENTIALW, CREDENTIAL_ATTRIBUTEW, CRED_ENUMERATE_ALL_CREDENTIALS, CRED_FLAGS, CRED_MAX_CREDENTIAL_BLOB_SIZE, CRED_MAX_GENERIC_TARGET_NAME_LENGTH, CRED_MAX_STRING_LENGTH, CRED_MAX_USERNAME_LENGTH, CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC, }; -use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; +use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialSearch, CredentialSearchApi, CredentialSearchResult}; use super::error::{Error as ErrorCode, Result}; /// The representation of a Windows Generic credential. @@ -322,6 +325,147 @@ impl CredentialBuilderApi for WinCredentialBuilder { } } +pub struct WinCredentialSearch {} + +/// Returns an instance of the Windows credential search. +/// +/// Can be specified to search by certain credential parameters +/// and by a query parameter. +pub fn default_credential_search() -> Box { + Box::new(WinCredentialSearch {}) +} + +impl CredentialSearchApi for WinCredentialSearch { + /// Specifies what parameter to search by and the query string + /// + /// Can return a [SearchError](Error::SearchError) + /// # Example + /// let search = keyring::Search::new().unwrap(); + /// let results = search.by("user", "Mr. Foo Bar"); + fn by(&self, by: &str, query: &str) -> CredentialSearchResult { + let results = match search_type(by, query) { + Ok(results) => results, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())) + }; + + let mut outer_map: HashMap> = HashMap::new(); + for result in results { + let mut inner_map: HashMap = HashMap::new(); + + inner_map.insert("Service".to_string(), result.comment); + inner_map.insert("User".to_string(), result.username); + + outer_map.insert(format!("Target {}", result.target_name), inner_map); + } + + Ok(outer_map) + } + +} + +// Type matching for search types +enum WinSearchType { + Target, + Service, + User +} + +// Match search type +fn search_type(by: &str, query: &str) -> Result>> { + let search_type = match by.to_ascii_lowercase().as_str() { + "target" => { WinSearchType::Target }, + "service" => { WinSearchType::Service }, + "user" => { WinSearchType::User } + _ => { return Err(ErrorCode::SearchError("Invalid search parameter, not Target, Service, or User".to_string())) } + }; + + search(&search_type, &query) + +} +// Perform search can return a regex error if the search parameter is invalid +fn search(search_type: &WinSearchType, search_parameter: &str) -> Result>> { + let credentials = get_all_credentials(); + + let re = format!(r#"(?i){}"#, search_parameter); + let regex = match Regex::new(re.as_str()) { + Ok(regex) => regex, + Err(err) => return Err(ErrorCode::SearchError( + format!("Regex Error, {}", err) + )) + }; + + let mut results = Vec::new(); + for credential in credentials { + let haystack = match search_type { + WinSearchType::Target => &credential.target_name, + WinSearchType::Service => &credential.comment, + WinSearchType::User => &credential.username + }; + if regex.is_match(haystack) { + results.push(credential); + } + } + + Ok(results) +} + +/// Returns a vector of credentials corresponding to entries in Windows Credential Manager. +/// +/// In Windows the target name is prepended with the credential type by default +/// i.e. LegacyGeneric:target=Example Target Name. +/// The type is stripped for string matching. +/// There is no guarantee that the enrties wil be in the same order as in +/// Windows Credential Manager. +fn get_all_credentials() -> Vec> { + let mut entries: Vec> = Vec::new(); + let mut count = 0; + let mut credentials_ptr = std::ptr::null_mut(); + + unsafe { + CredEnumerateW( + std::ptr::null(), + CRED_ENUMERATE_ALL_CREDENTIALS, + &mut count, + &mut credentials_ptr, + ); + } + + let credentials = + unsafe { std::slice::from_raw_parts::<&CREDENTIALW>(credentials_ptr as _, count as usize) }; + + for credential in credentials { + let target_name = unsafe { from_wstr(credential.TargetName) }; + // By default the target names are prepended with the credential type + // i.e. LegacyGeneric:target=Example Target Name. This is where + // The '=' is indexed to strip the prepended type + let index = match target_name.find('=') { + Some(index) => index, + None => 0 + }; + let target_name = target_name[ index + 1.. ].to_string(); + + let username; + if (unsafe { from_wstr(credential.UserName) } == "") { + username = String::from("NO USER"); + } else { + username = unsafe { from_wstr(credential.UserName) }; + } + let target_alias = unsafe { from_wstr(credential.TargetAlias) }; + let comment = unsafe { from_wstr(credential.Comment) }; + + entries.push( Box::new(WinCredential { + username, + target_name, + target_alias, + comment + })); + }; + + unsafe { CredFree(std::mem::transmute(credentials_ptr)) }; + + entries +} + fn extract_password(credential: &CREDENTIALW) -> Result { // get password blob let blob_pointer: *const u8 = credential.CredentialBlob; From bb919851dd80ec4a3710fbd5bd939d8825e49769 Mon Sep 17 00:00:00 2001 From: Nick Wimmers Date: Tue, 23 Apr 2024 14:02:34 -0400 Subject: [PATCH 02/14] Secret service search feature --- src/lib.rs | 4 +-- src/secret_service.rs | 62 ++++++++++++++++++++++++++++++++++++++++++- src/windows.rs | 4 ++- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0fac21a..585fbd4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -363,7 +363,7 @@ impl List { for (outer_key, inner_map) in search_result { println!("{outer_key}"); for (key, value) in inner_map { - println!("{key} {value}"); + println!("\t{key}:\t{value}"); } } Ok(()) @@ -380,7 +380,7 @@ impl List { for (outer_key, inner_map) in search_result { println!("{outer_key}"); for (key, value) in inner_map { - println!("{key} {value}"); + println!("\t{key}:\t{value}"); } count += 1; if count > max { diff --git a/src/secret_service.rs b/src/secret_service.rs index a9f4484..5a818fb 100644 --- a/src/secret_service.rs +++ b/src/secret_service.rs @@ -82,7 +82,9 @@ use std::collections::HashMap; use secret_service::blocking::{Collection, Item, SecretService}; use secret_service::{EncryptionType, Error}; -use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; +use super::credential::{ + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, + CredentialSearch, CredentialSearchApi, CredentialSearchResult }; use super::error::{decode_password, Error as ErrorCode, Result}; /// The representation of an item in the secret-service. @@ -327,6 +329,64 @@ impl CredentialBuilderApi for SsCredentialBuilder { } } +pub struct SsCredentialSearch {} + +pub fn default_credential_search() -> Box { + Box::new(SsCredentialSearch {}) +} + +impl CredentialSearchApi for SsCredentialSearch { + fn by(&self, by: &str, query: &str) -> CredentialSearchResult { + search_items(by, query) + } +} + +fn search_items(by: &str, query: &str) -> CredentialSearchResult { + let ss = match SecretService::connect(EncryptionType::Plain) { + Ok(connection) => connection, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; + + let collections = match ss.get_all_collections() { + Ok(collections) => collections, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; + + let mut search_map = HashMap::new(); + search_map.insert(by, query); + + let mut outer_map: HashMap> = HashMap::new(); + for collection in collections { + let search_results = match collection.search_items(search_map.clone()) { + Ok(results) => results, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; + + for result in search_results { + let attributes = match result.get_attributes() { + Ok(attributes) => attributes, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; + + let mut inner_map: HashMap = HashMap::new(); + + for (key, value) in attributes { + + inner_map.insert(key, value); + + let label = match result.get_label() { + Ok(label) => label, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; + + outer_map.insert(label.clone(), inner_map.clone()); + } + } + }; + + Ok(outer_map) +} + // // Secret Service utilities // diff --git a/src/windows.rs b/src/windows.rs index d4619ed..0998f0a 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -46,7 +46,9 @@ use windows_sys::Win32::Security::Credentials::{ CRED_MAX_USERNAME_LENGTH, CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC, }; -use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialSearch, CredentialSearchApi, CredentialSearchResult}; +use super::credential::{ + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, + CredentialSearch, CredentialSearchApi, CredentialSearchResult }; use super::error::{Error as ErrorCode, Result}; /// The representation of a Windows Generic credential. From 53b5e05175081753217d09fbd4e8eed9a0f9a42b Mon Sep 17 00:00:00 2001 From: Nick Wimmers Date: Wed, 24 Apr 2024 11:44:37 -0400 Subject: [PATCH 03/14] keyutils search feature --- src/keyutils.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/src/keyutils.rs b/src/keyutils.rs index d7a5245..029eaec 100644 --- a/src/keyutils.rs +++ b/src/keyutils.rs @@ -97,11 +97,14 @@ Alternatively, you can drop the secret-service credential store altogether with `--no-default-features` and `--features linux-no-secret-service`. */ +use std::collections::HashMap; + use super::credential::{ Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, + CredentialSearch, CredentialSearchApi, CredentialSearchResult }; use super::error::{decode_password, Error as ErrorCode, Result}; -use linux_keyutils::{KeyError, KeyRing, KeyRingIdentifier}; +use linux_keyutils::{KeyError, KeyRing, KeyRingIdentifier, KeyType, Permission}; /// Representation of a keyutils credential. /// @@ -294,6 +297,106 @@ impl CredentialBuilderApi for KeyutilsCredentialBuilder { } } +pub struct KeyutilsCredentialSearch {} + +pub fn default_credential_search() -> Box { + Box::new(KeyutilsCredentialSearch {}) +} + +impl CredentialSearchApi for KeyutilsCredentialSearch { + fn by(&self, by: &str, query: &str) -> CredentialSearchResult { + search_by_keyring(by, query) + } +} + +fn search_by_keyring(by: &str, query: &str) -> CredentialSearchResult { + + let by = match by { + "thread" => KeyRingIdentifier::Thread, + "process" => KeyRingIdentifier::Process, + "session" => KeyRingIdentifier::Session, + "user" => KeyRingIdentifier::User, + "user session" => KeyRingIdentifier::UserSession, + "group" => KeyRingIdentifier::Group, + _ => return Err(ErrorCode::SearchError("must match keyutils keyring identifiers: thread, process, session, user, user session, group".to_string())), + }; + + let ring = match KeyRing::from_special_id(by, false) { + Ok(ring) => ring, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; + + let result = match ring.search(query) { + Ok(result) => result, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; + + + let result_data = match result.metadata() { + Ok(data) => data, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; + + let key_type = match result_data.get_type() { + KeyType::KeyRing => "KeyRing".to_string(), + KeyType::BigKey => "BigKey".to_string(), + KeyType::Logon => "Logon".to_string(), + KeyType::User => "User".to_string(), + }; + + let permission_bits = result_data.get_perms().bits().to_be_bytes(); + + let permission_string = get_permissions(permission_bits[0]); + + let mut outer_map: HashMap> = HashMap::new(); + let mut inner_map: HashMap = HashMap::new(); + + inner_map.insert("perm".to_string(), permission_string); + inner_map.insert("gid".to_string(), result_data.get_gid().to_string()); + inner_map.insert("uid".to_string(), result_data.get_uid().to_string()); + inner_map.insert("ktype".to_string(), key_type); + + outer_map.insert(format!("ID: {} Description: {}", result.get_id().0, result_data.get_description()), inner_map); + + + Ok(outer_map) +} + +fn get_permissions(permission_data: u8) -> String { + let perm_types = [ + Permission::VIEW.bits(), + Permission::READ.bits(), + Permission::WRITE.bits(), + Permission::SEARCH.bits(), + Permission::LINK.bits(), + Permission::SETATTR.bits(), + Permission::ALL.bits() + ]; + + let perm_chars = [ + 'v', + 'r', + 'w', + 's', + 'l', + 'a', + '-' + ]; + + let mut perm_string = String::new(); + perm_string.push('-'); + + for i in (0..perm_types.len()).rev() { + if permission_data & perm_types[i] != 0 { + perm_string.push(perm_chars[i]); + } else { + perm_string.push('-'); + } + } + + perm_string +} + /// Map an underlying keyutils error to a platform-independent error with annotation. pub fn decode_error(err: KeyError) -> ErrorCode { match err { From 64d987db59cf76d9737115e6ec20acbc6a41cb13 Mon Sep 17 00:00:00 2001 From: Nick Wimmers <91842532+wiimmers@users.noreply.github.com> Date: Wed, 24 Apr 2024 22:58:33 -0400 Subject: [PATCH 04/14] MacOS search feature --- src/macos.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/macos.rs b/src/macos.rs index 42bb56c..22519a4 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -27,11 +27,15 @@ name as the target parameter to `Entry::new_with_target`. Any name other than one of the OS-supplied keychains (User, Common, System, and Dynamic) will be mapped to `User`. */ -use security_framework::base::Error; +use std::collections::HashMap; + +use security_framework::{item, base::Error}; use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain}; use security_framework::os::macos::passwords::find_generic_password; -use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; +use super::credential::{ + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, + CredentialSearch, CredentialSearchApi, CredentialSearchResult}; use super::error::{decode_password, Error as ErrorCode, Result}; /// The representation of a generic Keychain credential. @@ -221,6 +225,80 @@ fn get_keychain(cred: &MacCredential) -> Result { } } +pub struct MacCredentialSearch {} + +pub fn default_credential_search() -> Box { + Box::new(MacCredentialSearch {}) +} + +impl CredentialSearchApi for MacCredentialSearch { + fn by(&self, by: &str, query: &str) -> CredentialSearchResult { + search(by, query) + } +} + +enum MacSearchType { + Label, + Service, + Account +} + +fn search(by: &str, query: &str) -> CredentialSearchResult { + + let mut new_search = item::ItemSearchOptions::new(); + + let search_default = &mut new_search + .class(item::ItemClass::generic_password()) + .limit(item::Limit::All) + .load_attributes(true); + + let by = match by.to_ascii_lowercase().as_str() { + "label" => MacSearchType::Label, + "service" => MacSearchType::Service, + "account" => MacSearchType::Account, + _ => return Err(ErrorCode::SearchError("Invalid search parameter, not Label, Service, or Account".to_string())) + }; + + let search = match by { + MacSearchType::Label => search_default.label(query).search(), + MacSearchType::Service => search_default.service(query).search(), + MacSearchType::Account => search_default.account(query).search() + }; + + let mut outer_map: HashMap> = HashMap::new(); + + let results = match search { + Ok(items) => items, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())) + }; + + for item in results { + match to_credential_search_result(item.simplify_dict(), &mut outer_map) { + Ok(_) => {}, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())) + } + } + + Ok(outer_map) +} + +fn to_credential_search_result( + item: Option>, + outer_map: &mut HashMap> +) -> Result<()> { + let mut result = match item { + None => return Err(ErrorCode::SearchError("Search returned no items".to_string())), + Some(map) => map + }; + + let label = result.remove("labl").unwrap_or("EMPTY LABEL".to_string()); + + outer_map.insert(format!("Label: {}", label), result); + + Ok(()) +} + + /// Map a Mac API error to a crate error with appropriate annotation /// /// The MacOS error code values used here are from From 556b3612e8831afc13c0d45ab35057077d7672be Mon Sep 17 00:00:00 2001 From: wiimmers <91842532+wiimmers@users.noreply.github.com> Date: Mon, 29 Apr 2024 08:55:26 -0400 Subject: [PATCH 05/14] iOS search feature --- src/ios.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/src/ios.rs b/src/ios.rs index 5409b05..e0ddf36 100644 --- a/src/ios.rs +++ b/src/ios.rs @@ -16,12 +16,18 @@ wildcards when looking up credentials by attribute value.) On iOS, the target parameter is ignored, because there is only one keychain that can be targeted to store a generic credential. */ -use security_framework::base::Error; +use std::collections::HashMap; + +use security_framework::{item, base::Error}; use security_framework::passwords::{ delete_generic_password, get_generic_password, set_generic_password, }; -use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; + +use super::credential::{ + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, + CredentialSearch, CredentialSearchApi, CredentialSearchResult +}; use super::error::{decode_password, Error as ErrorCode, Result}; /// The representation of a generic Keychain credential. @@ -150,6 +156,92 @@ impl CredentialBuilderApi for IosCredentialBuilder { } } +pub struct IosCredentialSearch {} + +/// Returns an instance of the Ios credential search. +/// +/// This creates a new search structure. The by method +/// integrates with system_framework item search. Works similarly to +/// Mac, however, there are no labels so searching is done by Service, or Account. +pub fn default_credential_search() -> Box { + Box::new(IosCredentialSearch {}) +} + +impl CredentialSearchApi for IosCredentialSearch { + fn by(&self, by: &str, query: &str) -> CredentialSearchResult { + search(by, query) + } +} + +// Search type matching. +enum IosSearchType { + Service, + Account +} + +// Perform search, can throw a SearchError, returns a CredentialSearchResult. +// by must be "label", "service", or "account". +fn search(by: &str, query: &str) -> CredentialSearchResult { + + let mut new_search = item::ItemSearchOptions::new(); + + let search_default = &mut new_search + .class(item::ItemClass::generic_password()) + .limit(item::Limit::All) + .load_attributes(true); + + let by = match by.to_ascii_lowercase().as_str() { + "service" => IosSearchType::Service, + "account" => IosSearchType::Account, + _ => return Err(ErrorCode::SearchError("Invalid search parameter, not Label, Service, or Account".to_string())) + }; + + let search = match by { + IosSearchType::Service => search_default.service(query).search(), + IosSearchType::Account => search_default.account(query).search() + }; + + let mut outer_map: HashMap> = HashMap::new(); + + let results = match search { + Ok(items) => items, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())) + }; + + for item in results { + match to_credential_search_result(item.simplify_dict(), &mut outer_map) { + Ok(_) => {}, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())) + } + } + + Ok(outer_map) +} +// The returned item from search is converted to CredentialSearchResult type. +// If none, a SearchError is returned for no items found. The outer map's key +// is created with "user"@"service" to differentiate between credentials in the search. +fn to_credential_search_result( + item: Option>, + outer_map: &mut HashMap> +) -> Result<()> { + let result = match item { + None => return Err(ErrorCode::SearchError("Search returned no items".to_string())), + Some(map) => map + }; + +<<<<<<< Updated upstream + let label = "EMPTY LABEL".to_string(); +======= + let empty = &"EMPTY".to_string(); + let acct = result.get("acct").unwrap_or(empty); + let svce = result.get("svce").unwrap_or(empty); +>>>>>>> Stashed changes + + outer_map.insert(format!("{}@{}", acct, svce), result); + + Ok(()) +} + /// Map an iOS API error to a crate error with appropriate annotation /// /// The iOS error code values used here are from From 4399208ec5659324709e25641a5590cc11241574 Mon Sep 17 00:00:00 2001 From: Nick Wimmers <91842532+wiimmers@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:07:05 -0400 Subject: [PATCH 06/14] Updated Listing Returns string instead of using print statement. Allows for easier listing of all credentials in the future or custom search by user. --- src/lib.rs | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 585fbd4..70115eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -337,59 +337,51 @@ impl Search { pub struct List {} impl List { - pub fn list_credentials(search_result: CredentialSearchResult, limit: Limit) -> Result<()> { + pub fn list_credentials(search_result: CredentialSearchResult, limit: Limit) -> Result { match limit { Limit::All => { - match Self::list_all(search_result) { - Ok(_) => Ok(()), - Err(err) => return Err(Error::SearchError(err.to_string())) - } + Self::list_all(search_result) }, Limit::Max(max) => { - match Self::list_max(search_result, max) { - Ok(_) => Ok(()), - Err(err) => return Err(Error::SearchError(err.to_string())) - } - + Self::list_max(search_result, max) } } - - } - fn list_all(result: CredentialSearchResult) -> Result<()> { + fn list_all(result: CredentialSearchResult) -> Result { + let mut output = String::new(); match result { Ok(search_result) => { for (outer_key, inner_map) in search_result { - println!("{outer_key}"); + output.push_str(&format!("{}\n", outer_key)); for (key, value) in inner_map { - println!("\t{key}:\t{value}"); + output.push_str(&format!("\t{}:\t{}\n", key, value)); } } - Ok(()) + Ok(output) }, - Err(err) => return Err(Error::SearchError(err.to_string())) + Err(err) => Err(Error::SearchError(err.to_string())) } } - fn list_max(result: CredentialSearchResult, max: i64) -> Result<()> { - + fn list_max(result: CredentialSearchResult, max: i64) -> Result { + let mut output = String::new(); + let mut count = 1; match result { Ok(search_result) => { - let mut count = 1; for (outer_key, inner_map) in search_result { - println!("{outer_key}"); + output.push_str(&format!("{}\n", outer_key)); for (key, value) in inner_map { - println!("\t{key}:\t{value}"); + output.push_str(&format!("\t{}:\t{}\n", key, value)); } count += 1; if count > max { break; } } - Ok(()) + Ok(output) }, - Err(err) => return Err(Error::SearchError(err.to_string())) + Err(err) => Err(Error::SearchError(err.to_string())) } } } From 1cf13c726188986835c4fdb33fa04c74eb2bea1a Mon Sep 17 00:00:00 2001 From: wiimmers <91842532+wiimmers@users.noreply.github.com> Date: Mon, 29 Apr 2024 16:45:06 -0400 Subject: [PATCH 07/14] Docs and comments import formatting --- src/credential.rs | 6 ++++++ src/ios.rs | 10 ++-------- src/keyutils.rs | 8 ++++++-- src/lib.rs | 17 +++++++++++++++-- src/macos.rs | 16 +++++++++++++--- src/secret_service.rs | 9 ++++++++- src/windows.rs | 5 +++-- 7 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index d35ca9a..222b866 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -92,20 +92,26 @@ impl std::fmt::Debug for CredentialBuilder { /// A thread-safe implementation of the [CredentialBuilder API](CredentialBuilderApi). pub type CredentialBuilder = dyn CredentialBuilderApi + Send + Sync; +/// The API that [credential search](CredentialSearch) implements. pub trait CredentialSearchApi { fn by(&self, by: &str, query: &str) -> Result>>; } +/// A thread-safe implementation of the [CredentialSearch API](CredentialSearchApi). pub type CredentialSearch = dyn CredentialSearchApi + Send + Sync; +/// Type alias to shorten the long (and ugly) Credential Search Result HashMap. pub type CredentialSearchResult = Result>>; +/// The API that [credential list](CredentialList) implements. pub trait CredentialListApi { fn list_credentials(search_result: Result>>, limit: Limit) -> Result<()>; } +/// A thread-safe implementation of the [CredentialList API](CredentialListApi). pub type CredentialList = dyn CredentialListApi + Send + Sync; +/// Type matching enum, allows for constraint of the amount of results returned to the user. pub enum Limit { All, Max(i64) diff --git a/src/ios.rs b/src/ios.rs index e0ddf36..4e26400 100644 --- a/src/ios.rs +++ b/src/ios.rs @@ -224,20 +224,14 @@ fn to_credential_search_result( item: Option>, outer_map: &mut HashMap> ) -> Result<()> { - let result = match item { + let mut result = match item { None => return Err(ErrorCode::SearchError("Search returned no items".to_string())), Some(map) => map }; -<<<<<<< Updated upstream let label = "EMPTY LABEL".to_string(); -======= - let empty = &"EMPTY".to_string(); - let acct = result.get("acct").unwrap_or(empty); - let svce = result.get("svce").unwrap_or(empty); ->>>>>>> Stashed changes - outer_map.insert(format!("{}@{}", acct, svce), result); + outer_map.insert(format!("Label: {}", label), result); Ok(()) } diff --git a/src/keyutils.rs b/src/keyutils.rs index 029eaec..03b8581 100644 --- a/src/keyutils.rs +++ b/src/keyutils.rs @@ -299,6 +299,10 @@ impl CredentialBuilderApi for KeyutilsCredentialBuilder { pub struct KeyutilsCredentialSearch {} +/// Returns the Secret service default credential search structure. +/// +/// This creates a new search structure. The by method has concrete types to search by, +/// each corresponding to the different keyrings found within the kernel keyctl. pub fn default_credential_search() -> Box { Box::new(KeyutilsCredentialSearch {}) } @@ -308,7 +312,7 @@ impl CredentialSearchApi for KeyutilsCredentialSearch { search_by_keyring(by, query) } } - +// Search for credential items in the specified keyring. fn search_by_keyring(by: &str, query: &str) -> CredentialSearchResult { let by = match by { @@ -361,7 +365,7 @@ fn search_by_keyring(by: &str, query: &str) -> CredentialSearchResult { Ok(outer_map) } - +// Converts permission bits to their corresponding permission characters to match keyctl command in terminal. fn get_permissions(permission_data: u8) -> String { let perm_types = [ Permission::VIEW.bits(), diff --git a/src/lib.rs b/src/lib.rs index 70115eb..e8320be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -337,6 +337,11 @@ impl Search { pub struct List {} impl List { + /// List the credentials with given search result + /// + /// Takes CredentialSearchResult type and converts to a string + /// for printing. Matches the Limit type passed to constrain + /// the amount of results added to the string pub fn list_credentials(search_result: CredentialSearchResult, limit: Limit) -> Result { match limit { Limit::All => { @@ -347,7 +352,10 @@ impl List { } } } - + /// List all credential search results. + /// + /// Is the result of passing the Limit::All type + /// to list_credentials. fn list_all(result: CredentialSearchResult) -> Result { let mut output = String::new(); match result { @@ -363,7 +371,12 @@ impl List { Err(err) => Err(Error::SearchError(err.to_string())) } } - + /// List a certain amount of credential search results. + /// + /// Is the result of passing the Limit::Max(i64) type + /// to list_credentials. The 64 bit integer represents + /// the total of the results passed. + /// They are not sorted or filtered. fn list_max(result: CredentialSearchResult, max: i64) -> Result { let mut output = String::new(); let mut count = 1; diff --git a/src/macos.rs b/src/macos.rs index 22519a4..a2bf3df 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -35,7 +35,8 @@ use security_framework::os::macos::passwords::find_generic_password; use super::credential::{ Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, - CredentialSearch, CredentialSearchApi, CredentialSearchResult}; + CredentialSearch, CredentialSearchApi, CredentialSearchResult +}; use super::error::{decode_password, Error as ErrorCode, Result}; /// The representation of a generic Keychain credential. @@ -227,6 +228,11 @@ fn get_keychain(cred: &MacCredential) -> Result { pub struct MacCredentialSearch {} +/// Returns an instance of the Mac credential search. +/// +/// This creates a new search structure. The by method +/// integrates with system_framework item search. System_framework +/// only allows searching by Label, Service, or Account. pub fn default_credential_search() -> Box { Box::new(MacCredentialSearch {}) } @@ -236,13 +242,14 @@ impl CredentialSearchApi for MacCredentialSearch { search(by, query) } } - +// Type matching for search types. enum MacSearchType { Label, Service, Account } - +// Perform search, can throw a SearchError, returns a CredentialSearchResult. +// by must be "label", "service", or "account". fn search(by: &str, query: &str) -> CredentialSearchResult { let mut new_search = item::ItemSearchOptions::new(); @@ -282,6 +289,9 @@ fn search(by: &str, query: &str) -> CredentialSearchResult { Ok(outer_map) } +// The returned item from search is converted to CredentialSearchResult type. +// If none, a SearchError is returned for no items found. If results found, the "labl" +// key is removed and placed in the outer map's key to differentiate between results. fn to_credential_search_result( item: Option>, outer_map: &mut HashMap> diff --git a/src/secret_service.rs b/src/secret_service.rs index 5a818fb..5d55585 100644 --- a/src/secret_service.rs +++ b/src/secret_service.rs @@ -84,7 +84,8 @@ use secret_service::{EncryptionType, Error}; use super::credential::{ Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, - CredentialSearch, CredentialSearchApi, CredentialSearchResult }; + CredentialSearch, CredentialSearchApi, CredentialSearchResult +}; use super::error::{decode_password, Error as ErrorCode, Result}; /// The representation of an item in the secret-service. @@ -331,6 +332,11 @@ impl CredentialBuilderApi for SsCredentialBuilder { pub struct SsCredentialSearch {} +/// Returns the Secret service default credential search structure. +/// +/// This creates a new search structure. The by method has no concrete search types +/// like in Windows, iOS, and MacOS. The keys to these credentials can be whatever the user sets them to +/// and is displayed as a HashMap. pub fn default_credential_search() -> Box { Box::new(SsCredentialSearch {}) } @@ -341,6 +347,7 @@ impl CredentialSearchApi for SsCredentialSearch { } } +// Returns the items searched as a CredentialSearchResult fn search_items(by: &str, query: &str) -> CredentialSearchResult { let ss = match SecretService::connect(EncryptionType::Plain) { Ok(connection) => connection, diff --git a/src/windows.rs b/src/windows.rs index 0998f0a..e78a591 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -48,7 +48,8 @@ use windows_sys::Win32::Security::Credentials::{ use super::credential::{ Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, - CredentialSearch, CredentialSearchApi, CredentialSearchResult }; + CredentialSearch, CredentialSearchApi, CredentialSearchResult +}; use super::error::{Error as ErrorCode, Result}; /// The representation of a Windows Generic credential. @@ -384,7 +385,7 @@ fn search_type(by: &str, query: &str) -> Result>> { search(&search_type, &query) } -// Perform search can return a regex error if the search parameter is invalid +// Perform search, can return a regex error if the search parameter is invalid fn search(search_type: &WinSearchType, search_parameter: &str) -> Result>> { let credentials = get_all_credentials(); From 12ea6c902d9db84de9db152b3d4b4a15852a0765 Mon Sep 17 00:00:00 2001 From: wiimmers <91842532+wiimmers@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:18:45 -0400 Subject: [PATCH 08/14] update Cargo.toml Regex for Windows platform only --- Cargo.toml | 4 ++-- src/windows.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index be35f35..153b58d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ platform-freebsd = ["linux-secret-service"] platform-openbsd = ["linux-secret-service"] platform-macos = ["security-framework"] platform-ios = ["security-framework"] -platform-windows = ["windows-sys", "byteorder"] +platform-windows = ["windows-sys", "regex", "byteorder"] linux-secret-service = ["linux-secret-service-rt-async-io-crypto-rust"] linux-secret-service-rt-async-io-crypto-rust = ["secret-service/rt-async-io-crypto-rust"] linux-secret-service-rt-tokio-crypto-rust = ["secret-service/rt-tokio-crypto-rust"] @@ -32,7 +32,6 @@ windows-test-threading = [] [dependencies] lazy_static = "1" -regex = "1.10.4" [target.'cfg(target_os = "macos")'.dependencies] security-framework = { version = "2.6", optional = true } @@ -52,6 +51,7 @@ secret-service = { version = "3", optional = true } [target.'cfg(target_os = "windows")'.dependencies] byteorder = { version = "1.2", optional = true } +regex = { version = "1.10.4", optional = true } windows-sys = { version = "0.52", features = ["Win32_Foundation", "Win32_Security_Credentials"], optional = true } [[example]] diff --git a/src/windows.rs b/src/windows.rs index e78a591..2572d48 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -30,7 +30,7 @@ test suite of this crate, and they have been observed to fail in both Windows 10 and Windows 11. */ use byteorder::{ByteOrder, LittleEndian}; -pub use regex::Regex; +use regex::Regex; use std::collections::HashMap; use std::iter::once; use std::mem::MaybeUninit; From 51be847d2c0a0a27532a3501379c048838d23ec6 Mon Sep 17 00:00:00 2001 From: wiimmers <91842532+wiimmers@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:45:30 -0400 Subject: [PATCH 09/14] Windows tests --- src/windows.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/src/windows.rs b/src/windows.rs index 2572d48..59edfeb 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -358,7 +358,7 @@ impl CredentialSearchApi for WinCredentialSearch { inner_map.insert("Service".to_string(), result.comment); inner_map.insert("User".to_string(), result.username); - outer_map.insert(format!("Target {}", result.target_name), inner_map); + outer_map.insert(format!("{}", result.target_name), inner_map); } Ok(outer_map) @@ -556,7 +556,9 @@ mod tests { use crate::credential::CredentialPersistence; use crate::tests::{generate_random_string, generate_random_string_of_len}; - use crate::Entry; + use crate::{Entry, Search, List, Limit}; + + use std::collections::HashSet; #[test] fn test_persistence() { @@ -743,4 +745,88 @@ mod tests { .expect("Couldn't delete get-credential"); assert!(matches!(entry.get_password(), Err(ErrorCode::NoEntry))); } + + #[test] + fn test_search_by_target() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "search test password"; + entry + .set_password(password) + .expect("Not a windows credential"); + let result = Search::new() + .expect("Failed to build search") + .by("target", &name); + let list = List::list_credentials(result, Limit::All) + .expect("Failed to parse string from HashMap result"); + + let actual: &WinCredential = entry + .get_credential() + .downcast_ref() + .expect("Not a windows credential"); + + let expected = format!("{}\n\tService:\t{}\n\tUser:\t{}\n", actual.target_name, actual.comment, actual.username); + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); + assert_eq!(expected_set, result_set, "Search results do not match"); + entry + .delete_password() + .expect("Couldn't delete test-search-by-target"); + } + + #[test] + fn test_search_by_user() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "search test password"; + entry + .set_password(password) + .expect("Not a windows credential"); + let result = Search::new() + .expect("Failed to build search") + .by("user", &name); + let list = List::list_credentials(result, Limit::All) + .expect("Failed to parse string from HashMap result"); + + let actual: &WinCredential = entry + .get_credential() + .downcast_ref() + .expect("Not a windows credential"); + + let expected = format!("{}\n\tService:\t{}\n\tUser:\t{}\n", actual.target_name, actual.comment, actual.username); + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); + assert_eq!(expected_set, result_set, "Search results do not match"); + entry + .delete_password() + .expect("Couldn't delete test-search-by-user"); + } + + #[test] + fn test_search_by_service() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "search test password"; + entry + .set_password(password) + .expect("Not a windows credential"); + let result = Search::new() + .expect("Failed to build search") + .by("service", &name); + let list = List::list_credentials(result, Limit::All) + .expect("Failed to parse string from HashMap result"); + + let actual: &WinCredential = entry + .get_credential() + .downcast_ref() + .expect("Not a windows credential"); + + let expected = format!("{}\n\tService:\t{}\n\tUser:\t{}\n", actual.target_name, actual.comment, actual.username); + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); + assert_eq!(expected_set, result_set, "Search results do not match"); + entry + .delete_password() + .expect("Couldn't delete test-search-by-user"); + } } From 746304b04e48d704fd0ab5e643eab42f690b58dc Mon Sep 17 00:00:00 2001 From: Nick Wimmers Date: Tue, 30 Apr 2024 13:48:47 -0400 Subject: [PATCH 10/14] Secret service test --- src/secret_service.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/secret_service.rs b/src/secret_service.rs index 5d55585..465fd24 100644 --- a/src/secret_service.rs +++ b/src/secret_service.rs @@ -508,10 +508,12 @@ fn wrap(err: Error) -> Box { #[cfg(test)] mod tests { use crate::credential::CredentialPersistence; - use crate::{tests::generate_random_string, Entry, Error}; + use crate::{tests::generate_random_string, Entry, Error, Search, List, Limit}; use super::{default_credential_builder, SsCredential}; + use std::collections::HashSet; + #[test] fn test_persistence() { assert!(matches!( @@ -737,4 +739,37 @@ mod tests { .expect("Couldn't delete password for default collection"); assert!(matches!(entry3.get_password(), Err(Error::NoEntry))); } + + #[test] + fn test_search() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "search test password"; + entry + .set_password(password) + .expect("Not a Secret Service credential"); + let result = Search::new() + .expect("Failed to build search") + .by("service", &name); + let list = List::list_credentials(result, Limit::All) + .expect("Failed to parse string from HashMap result"); + + let actual: &SsCredential = entry + .get_credential() + .downcast_ref() + .expect("Not a Secret Service credential"); + + let mut expected = format!("{}\n", actual.label); + let attributes = &actual.attributes; + for (key, value) in attributes { + let attribute = format!("\t{}:\t{}\n", key, value); + expected.push_str(attribute.as_str()); + } + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); + assert_eq!(expected_set, result_set, "Search results do not match"); + entry + .delete_password() + .expect("Couldn't delete test-search-by-user"); + } } From 137d0eb4df0436c3c9cae550aaa633e102a504c1 Mon Sep 17 00:00:00 2001 From: Nick Wimmers Date: Tue, 30 Apr 2024 21:06:14 -0400 Subject: [PATCH 11/14] Keyutils test --- src/keyutils.rs | 79 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/src/keyutils.rs b/src/keyutils.rs index 03b8581..e6e5901 100644 --- a/src/keyutils.rs +++ b/src/keyutils.rs @@ -341,16 +341,11 @@ fn search_by_keyring(by: &str, query: &str) -> CredentialSearchResult { Err(err) => return Err(ErrorCode::SearchError(err.to_string())), }; - let key_type = match result_data.get_type() { - KeyType::KeyRing => "KeyRing".to_string(), - KeyType::BigKey => "BigKey".to_string(), - KeyType::Logon => "Logon".to_string(), - KeyType::User => "User".to_string(), - }; + let key_type = get_key_type(result_data.get_type()); let permission_bits = result_data.get_perms().bits().to_be_bytes(); - let permission_string = get_permissions(permission_bits[0]); + let permission_string = get_permission_chars(permission_bits[0]); let mut outer_map: HashMap> = HashMap::new(); let mut inner_map: HashMap = HashMap::new(); @@ -365,8 +360,16 @@ fn search_by_keyring(by: &str, query: &str) -> CredentialSearchResult { Ok(outer_map) } +fn get_key_type(key_type: KeyType) -> String { + match key_type { + KeyType::KeyRing => "KeyRing".to_string(), + KeyType::BigKey => "BigKey".to_string(), + KeyType::Logon => "Logon".to_string(), + KeyType::User => "User".to_string(), + } +} // Converts permission bits to their corresponding permission characters to match keyctl command in terminal. -fn get_permissions(permission_data: u8) -> String { +fn get_permission_chars(permission_data: u8) -> String { let perm_types = [ Permission::VIEW.bits(), Permission::READ.bits(), @@ -429,9 +432,12 @@ fn wrap(err: KeyError) -> Box { #[cfg(test)] mod tests { use crate::credential::CredentialPersistence; - use crate::{tests::generate_random_string, Entry, Error}; + use crate::keyutils::get_key_type; + use crate::{tests::generate_random_string, Entry, Error, Search, List, Limit}; - use super::{default_credential_builder, KeyutilsCredential}; + use std::collections::HashSet; + + use super::{default_credential_builder, get_permission_chars, KeyutilsCredential, KeyRing, KeyRingIdentifier}; #[test] fn test_persistence() { @@ -509,4 +515,57 @@ mod tests { .expect("Couldn't delete after get_credential"); assert!(matches!(entry.get_password(), Err(Error::NoEntry))); } + + #[test] + fn test_search() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "search test password"; + entry + .set_password(password) + .expect("Not a keyutils credential"); + let query = format!("keyring-rs:{}@{}", name, name); + let result = Search::new() + .expect("Failed to build search") + .by("session", &query); + let list = List::list_credentials(result, Limit::All) + .expect("Failed to parse string from HashMap result"); + + let actual: &KeyutilsCredential = entry + .get_credential() + .downcast_ref() + .expect("Not a keyutils credential"); + + let keyring = KeyRing::from_special_id(KeyRingIdentifier::Session, false) + .expect("No session keyring"); + let credential = keyring + .search(&actual.description) + .expect("Failed to downcast to linux-keyutils type"); + let metadata = credential + .metadata() + .expect("Failed to get credential metadata"); + + let mut expected = format!("ID: {} Description: {}\n", credential.get_id().0, actual.description); + expected.push_str(format!("\tgid:\t{}\n", metadata.get_gid()).as_str()); + expected.push_str(format!("\tuid:\t{}\n", metadata.get_uid()).as_str()); + expected.push_str(format!("\tperm:\t{}\n", get_permission_chars( + metadata + .get_perms() + .bits() + .to_be_bytes()[0] + )) + .as_str()); + expected.push_str(format!("\tktype:\t{}\n", get_key_type( + metadata + .get_type() + )) + .as_str()); + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); + assert_eq!(expected_set, result_set, "Search results do not match"); + entry + .delete_password() + .expect("Couldn't delete test-search-by-user"); + } + } From b6d39209279c74d14b9723ab9ff4a2ec459fd867 Mon Sep 17 00:00:00 2001 From: Nick Wimmers <91842532+wiimmers@users.noreply.github.com> Date: Tue, 30 Apr 2024 22:22:08 -0400 Subject: [PATCH 12/14] MacOS test --- src/macos.rs | 97 ++++++++++++++++++++++++++++++++++++++++++- src/secret_service.rs | 2 +- src/windows.rs | 58 ++++---------------------- 3 files changed, 104 insertions(+), 53 deletions(-) diff --git a/src/macos.rs b/src/macos.rs index a2bf3df..1b65063 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -301,9 +301,19 @@ fn to_credential_search_result( Some(map) => map }; + let mut formatted: HashMap = HashMap::new(); + + if result.get_key_value("svce").is_some() { + formatted.insert("Service".to_string(), result.get_key_value("svce").unwrap().1.to_string()); + } + + if result.get_key_value("acct").is_some() { + formatted.insert("Account".to_string(), result.get_key_value("acct").unwrap().1.to_string()); + } + let label = result.remove("labl").unwrap_or("EMPTY LABEL".to_string()); - outer_map.insert(format!("Label: {}", label), result); + outer_map.insert(format!("{}", label), formatted); Ok(()) } @@ -326,10 +336,13 @@ pub fn decode_error(err: Error) -> ErrorCode { #[cfg(test)] mod tests { + use security_framework::item; + use crate::credential::CredentialPersistence; - use crate::{tests::generate_random_string, Entry, Error}; + use crate::{tests::generate_random_string, Entry, Error, Search, List, Limit}; use super::{default_credential_builder, MacCredential}; + use std::collections::HashSet; #[test] fn test_persistence() { @@ -408,4 +421,84 @@ mod tests { .expect("Couldn't delete after get_credential"); assert!(matches!(entry.get_password(), Err(Error::NoEntry))); } + + fn test_search(by: &str) { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + entry + .set_password("test-search-password") + .expect("Failed to set password for test-search"); + let result = Search::new() + .expect("Failed to create new search") + .by(by, &name); + let list = List::list_credentials(result, Limit::All) + .expect("Failed to parse HashMap search result"); + let actual: &MacCredential = entry + .get_credential() + .downcast_ref() + .expect("Not a mac credential"); + + let mut new_search = item::ItemSearchOptions::new(); + + let search_default = &mut new_search + .class(item::ItemClass::generic_password()) + .limit(item::Limit::All) + .load_attributes(true); + + let vector_of_results = match by.to_ascii_lowercase().as_str() { + "account" => { + search_default + .account(actual.account.as_str()) + .search() + }, + "service" => { + search_default + .service(actual.account.as_str()) + .search() + }, + "label" => { + search_default + .label(actual.account.as_str()) + .search() + }, + _ => panic!() + }.expect("Failed to get vector of search results in system-framework"); + + let mut expected = String::new(); + + for item in vector_of_results { + let mut item = item.simplify_dict().expect("Unable to simplify to dictionary"); + let label = format!("{}\n", &item.remove("labl").expect("No label found")); + let service = format!("\tService:\t{}\n", actual.service); + let account = format!("\tAccount:\t{}\n", actual.account); + expected.push_str(&label); + expected.push_str(&service); + expected.push_str(&account); + } + + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); + assert_eq!(expected_set, result_set, "Search results do not match"); + + entry + .delete_password() + .expect("Failed to delete mac credential"); + + } + + #[test] + fn test_search_by_service() { + test_search("service") + } + + #[test] + fn test_search_by_label() { + test_search("label") + } + + #[test] + fn test_search_by_account() { + test_search("account") + } + } diff --git a/src/secret_service.rs b/src/secret_service.rs index 465fd24..4d4b389 100644 --- a/src/secret_service.rs +++ b/src/secret_service.rs @@ -770,6 +770,6 @@ mod tests { assert_eq!(expected_set, result_set, "Search results do not match"); entry .delete_password() - .expect("Couldn't delete test-search-by-user"); + .expect("Couldn't delete test-search"); } } diff --git a/src/windows.rs b/src/windows.rs index 59edfeb..b05868c 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -746,8 +746,7 @@ mod tests { assert!(matches!(entry.get_password(), Err(ErrorCode::NoEntry))); } - #[test] - fn test_search_by_target() { + fn test_search(by: &str) { let name = generate_random_string(); let entry = entry_new(&name, &name); let password = "search test password"; @@ -756,7 +755,7 @@ mod tests { .expect("Not a windows credential"); let result = Search::new() .expect("Failed to build search") - .by("target", &name); + .by(by, &name); let list = List::list_credentials(result, Limit::All) .expect("Failed to parse string from HashMap result"); @@ -776,57 +775,16 @@ mod tests { #[test] fn test_search_by_user() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "search test password"; - entry - .set_password(password) - .expect("Not a windows credential"); - let result = Search::new() - .expect("Failed to build search") - .by("user", &name); - let list = List::list_credentials(result, Limit::All) - .expect("Failed to parse string from HashMap result"); - - let actual: &WinCredential = entry - .get_credential() - .downcast_ref() - .expect("Not a windows credential"); - - let expected = format!("{}\n\tService:\t{}\n\tUser:\t{}\n", actual.target_name, actual.comment, actual.username); - let expected_set: HashSet<&str> = expected.lines().collect(); - let result_set: HashSet<&str> = list.lines().collect(); - assert_eq!(expected_set, result_set, "Search results do not match"); - entry - .delete_password() - .expect("Couldn't delete test-search-by-user"); + test_search("user") } #[test] fn test_search_by_service() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "search test password"; - entry - .set_password(password) - .expect("Not a windows credential"); - let result = Search::new() - .expect("Failed to build search") - .by("service", &name); - let list = List::list_credentials(result, Limit::All) - .expect("Failed to parse string from HashMap result"); - - let actual: &WinCredential = entry - .get_credential() - .downcast_ref() - .expect("Not a windows credential"); + test_search("service") + } - let expected = format!("{}\n\tService:\t{}\n\tUser:\t{}\n", actual.target_name, actual.comment, actual.username); - let expected_set: HashSet<&str> = expected.lines().collect(); - let result_set: HashSet<&str> = list.lines().collect(); - assert_eq!(expected_set, result_set, "Search results do not match"); - entry - .delete_password() - .expect("Couldn't delete test-search-by-user"); + #[test] + fn test_search_by_target() { + test_search("target") } } From 2e300716f9a04bdcbb78b793133d5c8d160add21 Mon Sep 17 00:00:00 2001 From: Nick Wimmers <91842532+wiimmers@users.noreply.github.com> Date: Wed, 1 May 2024 14:18:34 -0400 Subject: [PATCH 13/14] cargo fmt, clippy --- src/credential.rs | 19 +- src/error.rs | 248 +++++++-------- src/ios.rs | 54 ++-- src/keyutils.rs | 124 ++++---- src/lib.rs | 51 ++- src/macos.rs | 127 ++++---- src/mock.rs | 726 +++++++++++++++++++++--------------------- src/secret_service.rs | 49 ++- src/windows.rs | 145 +++++---- 9 files changed, 772 insertions(+), 771 deletions(-) diff --git a/src/credential.rs b/src/credential.rs index 222b866..b9547b7 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -9,7 +9,7 @@ in a thread-safe way, a requirement captured in the [CredentialBuilder] and [CredentialApi] types that wrap them. */ use super::Result; -use std::{collections::HashMap, any::Any}; +use std::{any::Any, collections::HashMap}; /// The API that [credentials](Credential) implement. pub trait CredentialApi { @@ -94,25 +94,28 @@ pub type CredentialBuilder = dyn CredentialBuilderApi + Send + Sync; /// The API that [credential search](CredentialSearch) implements. pub trait CredentialSearchApi { - fn by(&self, by: &str, query: &str) -> Result>>; + fn by(&self, by: &str, query: &str) -> Result>>; } /// A thread-safe implementation of the [CredentialSearch API](CredentialSearchApi). -pub type CredentialSearch = dyn CredentialSearchApi + Send + Sync; +pub type CredentialSearch = dyn CredentialSearchApi + Send + Sync; /// Type alias to shorten the long (and ugly) Credential Search Result HashMap. pub type CredentialSearchResult = Result>>; /// The API that [credential list](CredentialList) implements. pub trait CredentialListApi { - fn list_credentials(search_result: Result>>, limit: Limit) -> Result<()>; + fn list_credentials( + search_result: Result>>, + limit: Limit, + ) -> Result<()>; } /// A thread-safe implementation of the [CredentialList API](CredentialListApi). -pub type CredentialList = dyn CredentialListApi + Send + Sync; +pub type CredentialList = dyn CredentialListApi + Send + Sync; -/// Type matching enum, allows for constraint of the amount of results returned to the user. +/// Type matching enum, allows for constraint of the amount of results returned to the user. pub enum Limit { - All, - Max(i64) + All, + Max(i64), } diff --git a/src/error.rs b/src/error.rs index e0f7a3a..4ba38e3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,124 +1,124 @@ -/*! - -Platform-independent error model. - -There is an escape hatch here for surfacing platform-specific -error information returned by the platform-specific storage provider, -but (like all credential-related data) the concrete objects returned -must be both Send and Sync so credentials remain Send + Sync. -(Since most platform errors are integer error codes, this requirement -is not much of a burden on the platform-specific store providers.) - - */ - -use crate::Credential; - -#[derive(Debug)] -/// Each variant of the `Error` enum provides a summary of the error. -/// More details, if relevant, are contained in the associated value, -/// which may be platform-specific. -/// -/// Because future releases may add variants to this enum, clients should -/// always be prepared for that. -#[non_exhaustive] -pub enum Error { - /// This indicates runtime failure in the underlying - /// platform storage system. The details of the failure can - /// be retrieved from the attached platform error. - PlatformFailure(Box), - /// This indicates that the underlying secure storage - /// holding saved items could not be accessed. Typically this - /// is because of access rules in the platform; for example, it - /// might be that the credential store is locked. The underlying - /// platform error will typically give the reason. - NoStorageAccess(Box), - /// This indicates that there is no underlying credential - /// entry in the platform for this entry. Either one was - /// never set, or it was deleted. - NoEntry, - /// This indicates that the retrieved password blob was not - /// a UTF-8 string. The underlying bytes are available - /// for examination in the attached value. - BadEncoding(Vec), - /// This indicates that one of the entry's credential - /// attributes exceeded a - /// length limit in the underlying platform. The - /// attached values give the name of the attribute and - /// the platform length limit that was exceeded. - TooLong(String, u32), - /// This indicates that one of the entry's required credential - /// attributes was invalid. The - /// attached value gives the name of the attribute - /// and the reason it's invalid. - Invalid(String, String), - /// This indicates that there is more than one credential found in the store - /// that matches the entry. Its value is a vector of the matching credentials. - Ambiguous(Vec>), - - SearchError(String), -} - -pub type Result = std::result::Result; - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Error::PlatformFailure(err) => write!(f, "Platform secure storage failure: {err}"), - Error::NoStorageAccess(err) => { - write!(f, "Couldn't access platform secure storage: {err}") - } - Error::NoEntry => write!(f, "No matching entry found in secure storage"), - Error::BadEncoding(_) => write!(f, "Password cannot be UTF-8 encoded"), - Error::TooLong(name, len) => write!( - f, - "Attribute '{name}' is longer than platform limit of {len} chars" - ), - Error::Invalid(attr, reason) => { - write!(f, "Attribute {attr} is invalid: {reason}") - } - Error::Ambiguous(items) => { - write!( - f, - "Entry is matched by {} credendials: {items:?}", - items.len(), - ) - } - Error::SearchError(reason) => { - write!(f, "Error searching for credential: {}", reason) - } - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::PlatformFailure(err) => Some(err.as_ref()), - Error::NoStorageAccess(err) => Some(err.as_ref()), - _ => None, - } - } -} - -/// Try to interpret a byte vector as a password string -pub fn decode_password(bytes: Vec) -> Result { - String::from_utf8(bytes).map_err(|err| Error::BadEncoding(err.into_bytes())) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_bad_password() { - // malformed sequences here taken from: - // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt - for bytes in [b"\x80".to_vec(), b"\xbf".to_vec(), b"\xed\xa0\xa0".to_vec()] { - match decode_password(bytes.clone()) { - Err(Error::BadEncoding(str)) => assert_eq!(str, bytes), - Err(other) => panic!("Bad password ({bytes:?}) decode gave wrong error: {other}"), - Ok(s) => panic!("Bad password ({bytes:?}) decode gave results: {s:?}"), - } - } - } -} +/*! + +Platform-independent error model. + +There is an escape hatch here for surfacing platform-specific +error information returned by the platform-specific storage provider, +but (like all credential-related data) the concrete objects returned +must be both Send and Sync so credentials remain Send + Sync. +(Since most platform errors are integer error codes, this requirement +is not much of a burden on the platform-specific store providers.) + + */ + +use crate::Credential; + +#[derive(Debug)] +/// Each variant of the `Error` enum provides a summary of the error. +/// More details, if relevant, are contained in the associated value, +/// which may be platform-specific. +/// +/// Because future releases may add variants to this enum, clients should +/// always be prepared for that. +#[non_exhaustive] +pub enum Error { + /// This indicates runtime failure in the underlying + /// platform storage system. The details of the failure can + /// be retrieved from the attached platform error. + PlatformFailure(Box), + /// This indicates that the underlying secure storage + /// holding saved items could not be accessed. Typically this + /// is because of access rules in the platform; for example, it + /// might be that the credential store is locked. The underlying + /// platform error will typically give the reason. + NoStorageAccess(Box), + /// This indicates that there is no underlying credential + /// entry in the platform for this entry. Either one was + /// never set, or it was deleted. + NoEntry, + /// This indicates that the retrieved password blob was not + /// a UTF-8 string. The underlying bytes are available + /// for examination in the attached value. + BadEncoding(Vec), + /// This indicates that one of the entry's credential + /// attributes exceeded a + /// length limit in the underlying platform. The + /// attached values give the name of the attribute and + /// the platform length limit that was exceeded. + TooLong(String, u32), + /// This indicates that one of the entry's required credential + /// attributes was invalid. The + /// attached value gives the name of the attribute + /// and the reason it's invalid. + Invalid(String, String), + /// This indicates that there is more than one credential found in the store + /// that matches the entry. Its value is a vector of the matching credentials. + Ambiguous(Vec>), + + SearchError(String), +} + +pub type Result = std::result::Result; + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Error::PlatformFailure(err) => write!(f, "Platform secure storage failure: {err}"), + Error::NoStorageAccess(err) => { + write!(f, "Couldn't access platform secure storage: {err}") + } + Error::NoEntry => write!(f, "No matching entry found in secure storage"), + Error::BadEncoding(_) => write!(f, "Password cannot be UTF-8 encoded"), + Error::TooLong(name, len) => write!( + f, + "Attribute '{name}' is longer than platform limit of {len} chars" + ), + Error::Invalid(attr, reason) => { + write!(f, "Attribute {attr} is invalid: {reason}") + } + Error::Ambiguous(items) => { + write!( + f, + "Entry is matched by {} credendials: {items:?}", + items.len(), + ) + } + Error::SearchError(reason) => { + write!(f, "Error searching for credential: {}", reason) + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::PlatformFailure(err) => Some(err.as_ref()), + Error::NoStorageAccess(err) => Some(err.as_ref()), + _ => None, + } + } +} + +/// Try to interpret a byte vector as a password string +pub fn decode_password(bytes: Vec) -> Result { + String::from_utf8(bytes).map_err(|err| Error::BadEncoding(err.into_bytes())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bad_password() { + // malformed sequences here taken from: + // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt + for bytes in [b"\x80".to_vec(), b"\xbf".to_vec(), b"\xed\xa0\xa0".to_vec()] { + match decode_password(bytes.clone()) { + Err(Error::BadEncoding(str)) => assert_eq!(str, bytes), + Err(other) => panic!("Bad password ({bytes:?}) decode gave wrong error: {other}"), + Ok(s) => panic!("Bad password ({bytes:?}) decode gave results: {s:?}"), + } + } + } +} diff --git a/src/ios.rs b/src/ios.rs index 4e26400..f36e8ed 100644 --- a/src/ios.rs +++ b/src/ios.rs @@ -18,15 +18,14 @@ that can be targeted to store a generic credential. */ use std::collections::HashMap; -use security_framework::{item, base::Error}; use security_framework::passwords::{ delete_generic_password, get_generic_password, set_generic_password, }; - +use security_framework::{base::Error, item}; use super::credential::{ - Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, - CredentialSearch, CredentialSearchApi, CredentialSearchResult + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialSearch, + CredentialSearchApi, CredentialSearchResult, }; use super::error::{decode_password, Error as ErrorCode, Result}; @@ -160,8 +159,8 @@ pub struct IosCredentialSearch {} /// Returns an instance of the Ios credential search. /// -/// This creates a new search structure. The by method -/// integrates with system_framework item search. Works similarly to +/// This creates a new search structure. The by method +/// integrates with system_framework item search. Works similarly to /// Mac, however, there are no labels so searching is done by Service, or Account. pub fn default_credential_search() -> Box { Box::new(IosCredentialSearch {}) @@ -173,18 +172,17 @@ impl CredentialSearchApi for IosCredentialSearch { } } -// Search type matching. +// Search type matching. enum IosSearchType { - Service, - Account + Service, + Account, } // Perform search, can throw a SearchError, returns a CredentialSearchResult. // by must be "label", "service", or "account". fn search(by: &str, query: &str) -> CredentialSearchResult { + let mut new_search = item::ItemSearchOptions::new(); - let mut new_search = item::ItemSearchOptions::new(); - let search_default = &mut new_search .class(item::ItemClass::generic_password()) .limit(item::Limit::All) @@ -193,40 +191,48 @@ fn search(by: &str, query: &str) -> CredentialSearchResult { let by = match by.to_ascii_lowercase().as_str() { "service" => IosSearchType::Service, "account" => IosSearchType::Account, - _ => return Err(ErrorCode::SearchError("Invalid search parameter, not Label, Service, or Account".to_string())) + _ => { + return Err(ErrorCode::SearchError( + "Invalid search parameter, not Label, Service, or Account".to_string(), + )) + } }; let search = match by { IosSearchType::Service => search_default.service(query).search(), - IosSearchType::Account => search_default.account(query).search() + IosSearchType::Account => search_default.account(query).search(), }; - let mut outer_map: HashMap> = HashMap::new(); + let mut outer_map: HashMap> = HashMap::new(); let results = match search { Ok(items) => items, - Err(err) => return Err(ErrorCode::SearchError(err.to_string())) - }; + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; for item in results { match to_credential_search_result(item.simplify_dict(), &mut outer_map) { - Ok(_) => {}, - Err(err) => return Err(ErrorCode::SearchError(err.to_string())) + Ok(_) => {} + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), } } Ok(outer_map) } -// The returned item from search is converted to CredentialSearchResult type. -// If none, a SearchError is returned for no items found. The outer map's key -// is created with "user"@"service" to differentiate between credentials in the search. +// The returned item from search is converted to CredentialSearchResult type. +// If none, a SearchError is returned for no items found. The outer map's key +// is created with "user"@"service" to differentiate between credentials in the search. fn to_credential_search_result( item: Option>, - outer_map: &mut HashMap> + outer_map: &mut HashMap>, ) -> Result<()> { let mut result = match item { - None => return Err(ErrorCode::SearchError("Search returned no items".to_string())), - Some(map) => map + None => { + return Err(ErrorCode::SearchError( + "Search returned no items".to_string(), + )) + } + Some(map) => map, }; let label = "EMPTY LABEL".to_string(); diff --git a/src/keyutils.rs b/src/keyutils.rs index e6e5901..a2d8b56 100644 --- a/src/keyutils.rs +++ b/src/keyutils.rs @@ -101,7 +101,7 @@ use std::collections::HashMap; use super::credential::{ Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, - CredentialSearch, CredentialSearchApi, CredentialSearchResult + CredentialSearch, CredentialSearchApi, CredentialSearchResult, }; use super::error::{decode_password, Error as ErrorCode, Result}; use linux_keyutils::{KeyError, KeyRing, KeyRingIdentifier, KeyType, Permission}; @@ -299,10 +299,10 @@ impl CredentialBuilderApi for KeyutilsCredentialBuilder { pub struct KeyutilsCredentialSearch {} -/// Returns the Secret service default credential search structure. +/// Returns the Secret service default credential search structure. /// -/// This creates a new search structure. The by method has concrete types to search by, -/// each corresponding to the different keyrings found within the kernel keyctl. +/// This creates a new search structure. The by method has concrete types to search by, +/// each corresponding to the different keyrings found within the kernel keyctl. pub fn default_credential_search() -> Box { Box::new(KeyutilsCredentialSearch {}) } @@ -312,9 +312,8 @@ impl CredentialSearchApi for KeyutilsCredentialSearch { search_by_keyring(by, query) } } -// Search for credential items in the specified keyring. +// Search for credential items in the specified keyring. fn search_by_keyring(by: &str, query: &str) -> CredentialSearchResult { - let by = match by { "thread" => KeyRingIdentifier::Thread, "process" => KeyRingIdentifier::Process, @@ -326,81 +325,78 @@ fn search_by_keyring(by: &str, query: &str) -> CredentialSearchResult { }; let ring = match KeyRing::from_special_id(by, false) { - Ok(ring) => ring, + Ok(ring) => ring, Err(err) => return Err(ErrorCode::SearchError(err.to_string())), - }; + }; let result = match ring.search(query) { Ok(result) => result, Err(err) => return Err(ErrorCode::SearchError(err.to_string())), - }; - + }; let result_data = match result.metadata() { Ok(data) => data, Err(err) => return Err(ErrorCode::SearchError(err.to_string())), - }; + }; let key_type = get_key_type(result_data.get_type()); let permission_bits = result_data.get_perms().bits().to_be_bytes(); - let permission_string = get_permission_chars(permission_bits[0]); - - let mut outer_map: HashMap> = HashMap::new(); - let mut inner_map: HashMap = HashMap::new(); + let permission_string = get_permission_chars(permission_bits[0]); + + let mut outer_map: HashMap> = HashMap::new(); + let mut inner_map: HashMap = HashMap::new(); inner_map.insert("perm".to_string(), permission_string); inner_map.insert("gid".to_string(), result_data.get_gid().to_string()); inner_map.insert("uid".to_string(), result_data.get_uid().to_string()); inner_map.insert("ktype".to_string(), key_type); - outer_map.insert(format!("ID: {} Description: {}", result.get_id().0, result_data.get_description()), inner_map); - + outer_map.insert( + format!( + "ID: {} Description: {}", + result.get_id().0, + result_data.get_description() + ), + inner_map, + ); Ok(outer_map) } fn get_key_type(key_type: KeyType) -> String { match key_type { - KeyType::KeyRing => "KeyRing".to_string(), + KeyType::KeyRing => "KeyRing".to_string(), KeyType::BigKey => "BigKey".to_string(), KeyType::Logon => "Logon".to_string(), - KeyType::User => "User".to_string(), + KeyType::User => "User".to_string(), } } -// Converts permission bits to their corresponding permission characters to match keyctl command in terminal. +// Converts permission bits to their corresponding permission characters to match keyctl command in terminal. fn get_permission_chars(permission_data: u8) -> String { let perm_types = [ - Permission::VIEW.bits(), + Permission::VIEW.bits(), Permission::READ.bits(), - Permission::WRITE.bits(), - Permission::SEARCH.bits(), - Permission::LINK.bits(), - Permission::SETATTR.bits(), - Permission::ALL.bits() + Permission::WRITE.bits(), + Permission::SEARCH.bits(), + Permission::LINK.bits(), + Permission::SETATTR.bits(), + Permission::ALL.bits(), ]; - let perm_chars = [ - 'v', - 'r', - 'w', - 's', - 'l', - 'a', - '-' - ]; + let perm_chars = ['v', 'r', 'w', 's', 'l', 'a', '-']; - let mut perm_string = String::new(); + let mut perm_string = String::new(); perm_string.push('-'); for i in (0..perm_types.len()).rev() { if permission_data & perm_types[i] != 0 { perm_string.push(perm_chars[i]); } else { - perm_string.push('-'); + perm_string.push('-'); } } - + perm_string } @@ -433,11 +429,14 @@ fn wrap(err: KeyError) -> Box { mod tests { use crate::credential::CredentialPersistence; use crate::keyutils::get_key_type; - use crate::{tests::generate_random_string, Entry, Error, Search, List, Limit}; + use crate::{tests::generate_random_string, Entry, Error, Limit, List, Search}; use std::collections::HashSet; - use super::{default_credential_builder, get_permission_chars, KeyutilsCredential, KeyRing, KeyRingIdentifier}; + use super::{ + default_credential_builder, get_permission_chars, KeyRing, KeyRingIdentifier, + KeyutilsCredential, + }; #[test] fn test_persistence() { @@ -518,12 +517,12 @@ mod tests { #[test] fn test_search() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "search test password"; + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "search test password"; entry .set_password(password) - .expect("Not a keyutils credential"); + .expect("Not a keyutils credential"); let query = format!("keyring-rs:{}@{}", name, name); let result = Search::new() .expect("Failed to build search") @@ -534,38 +533,37 @@ mod tests { let actual: &KeyutilsCredential = entry .get_credential() .downcast_ref() - .expect("Not a keyutils credential"); + .expect("Not a keyutils credential"); let keyring = KeyRing::from_special_id(KeyRingIdentifier::Session, false) - .expect("No session keyring"); + .expect("No session keyring"); let credential = keyring .search(&actual.description) - .expect("Failed to downcast to linux-keyutils type"); + .expect("Failed to downcast to linux-keyutils type"); let metadata = credential .metadata() .expect("Failed to get credential metadata"); - let mut expected = format!("ID: {} Description: {}\n", credential.get_id().0, actual.description); + let mut expected = format!( + "ID: {} Description: {}\n", + credential.get_id().0, + actual.description + ); expected.push_str(format!("\tgid:\t{}\n", metadata.get_gid()).as_str()); expected.push_str(format!("\tuid:\t{}\n", metadata.get_uid()).as_str()); - expected.push_str(format!("\tperm:\t{}\n", get_permission_chars( - metadata - .get_perms() - .bits() - .to_be_bytes()[0] - )) - .as_str()); - expected.push_str(format!("\tktype:\t{}\n", get_key_type( - metadata - .get_type() - )) - .as_str()); - let expected_set: HashSet<&str> = expected.lines().collect(); - let result_set: HashSet<&str> = list.lines().collect(); + expected.push_str( + format!( + "\tperm:\t{}\n", + get_permission_chars(metadata.get_perms().bits().to_be_bytes()[0]) + ) + .as_str(), + ); + expected.push_str(format!("\tktype:\t{}\n", get_key_type(metadata.get_type())).as_str()); + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); assert_eq!(expected_set, result_set, "Search results do not match"); entry .delete_password() .expect("Couldn't delete test-search-by-user"); } - } diff --git a/src/lib.rs b/src/lib.rs index e8320be..3ece0da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,9 @@ entry creation doesn't go through the platform credential manager. It's fine to create an entry on one thread and then immediately use it on a different thread. This is thoroughly tested on all platforms.) */ -pub use credential::{Credential, CredentialBuilder, CredentialSearch, CredentialSearchResult, CredentialList, Limit}; +pub use credential::{ + Credential, CredentialBuilder, CredentialList, CredentialSearch, CredentialSearchResult, Limit, +}; pub use error::{Error, Result}; // Included keystore implementations and default choice thereof. @@ -307,24 +309,23 @@ impl Entry { } fn default_credential_search() -> Result { - let credentials = default::default_credential_search(); - Ok(Search {inner: credentials}) + let credentials = default::default_credential_search(); + Ok(Search { inner: credentials }) } - pub struct Search { - inner: Box + inner: Box, } impl Search { /// Create a new instance of the Credential Search. - /// + /// /// The default credential search is used. pub fn new() -> Result { default_credential_search() } /// Specifies what parameter to search by and the query string - /// + /// /// Can return a [SearchError](Error::SearchError) /// # Example /// let search = keyring::Search::new().unwrap(); @@ -338,25 +339,21 @@ pub struct List {} impl List { /// List the credentials with given search result - /// + /// /// Takes CredentialSearchResult type and converts to a string /// for printing. Matches the Limit type passed to constrain /// the amount of results added to the string pub fn list_credentials(search_result: CredentialSearchResult, limit: Limit) -> Result { match limit { - Limit::All => { - Self::list_all(search_result) - }, - Limit::Max(max) => { - Self::list_max(search_result, max) - } + Limit::All => Self::list_all(search_result), + Limit::Max(max) => Self::list_max(search_result, max), } } /// List all credential search results. - /// - /// Is the result of passing the Limit::All type + /// + /// Is the result of passing the Limit::All type /// to list_credentials. - fn list_all(result: CredentialSearchResult) -> Result { + fn list_all(result: CredentialSearchResult) -> Result { let mut output = String::new(); match result { Ok(search_result) => { @@ -367,19 +364,19 @@ impl List { } } Ok(output) - }, - Err(err) => Err(Error::SearchError(err.to_string())) + } + Err(err) => Err(Error::SearchError(err.to_string())), } } /// List a certain amount of credential search results. - /// - /// Is the result of passing the Limit::Max(i64) type + /// + /// Is the result of passing the Limit::Max(i64) type /// to list_credentials. The 64 bit integer represents - /// the total of the results passed. + /// the total of the results passed. /// They are not sorted or filtered. fn list_max(result: CredentialSearchResult, max: i64) -> Result { let mut output = String::new(); - let mut count = 1; + let mut count = 1; match result { Ok(search_result) => { for (outer_key, inner_map) in search_result { @@ -387,14 +384,14 @@ impl List { for (key, value) in inner_map { output.push_str(&format!("\t{}:\t{}\n", key, value)); } - count += 1; + count += 1; if count > max { - break; + break; } } Ok(output) - }, - Err(err) => Err(Error::SearchError(err.to_string())) + } + Err(err) => Err(Error::SearchError(err.to_string())), } } } diff --git a/src/macos.rs b/src/macos.rs index 1b65063..cebb380 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -29,13 +29,13 @@ will be mapped to `User`. */ use std::collections::HashMap; -use security_framework::{item, base::Error}; use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain}; use security_framework::os::macos::passwords::find_generic_password; +use security_framework::{base::Error, item}; use super::credential::{ - Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, - CredentialSearch, CredentialSearchApi, CredentialSearchResult + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialSearch, + CredentialSearchApi, CredentialSearchResult, }; use super::error::{decode_password, Error as ErrorCode, Result}; @@ -230,7 +230,7 @@ pub struct MacCredentialSearch {} /// Returns an instance of the Mac credential search. /// -/// This creates a new search structure. The by method +/// This creates a new search structure. The by method /// integrates with system_framework item search. System_framework /// only allows searching by Label, Service, or Account. pub fn default_credential_search() -> Box { @@ -242,18 +242,17 @@ impl CredentialSearchApi for MacCredentialSearch { search(by, query) } } -// Type matching for search types. +// Type matching for search types. enum MacSearchType { - Label, - Service, - Account + Label, + Service, + Account, } // Perform search, can throw a SearchError, returns a CredentialSearchResult. // by must be "label", "service", or "account". fn search(by: &str, query: &str) -> CredentialSearchResult { + let mut new_search = item::ItemSearchOptions::new(); - let mut new_search = item::ItemSearchOptions::new(); - let search_default = &mut new_search .class(item::ItemClass::generic_password()) .limit(item::Limit::All) @@ -263,62 +262,75 @@ fn search(by: &str, query: &str) -> CredentialSearchResult { "label" => MacSearchType::Label, "service" => MacSearchType::Service, "account" => MacSearchType::Account, - _ => return Err(ErrorCode::SearchError("Invalid search parameter, not Label, Service, or Account".to_string())) + _ => { + return Err(ErrorCode::SearchError( + "Invalid search parameter, not Label, Service, or Account".to_string(), + )) + } }; let search = match by { MacSearchType::Label => search_default.label(query).search(), MacSearchType::Service => search_default.service(query).search(), - MacSearchType::Account => search_default.account(query).search() + MacSearchType::Account => search_default.account(query).search(), }; - let mut outer_map: HashMap> = HashMap::new(); + let mut outer_map: HashMap> = HashMap::new(); let results = match search { Ok(items) => items, - Err(err) => return Err(ErrorCode::SearchError(err.to_string())) - }; + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; for item in results { match to_credential_search_result(item.simplify_dict(), &mut outer_map) { - Ok(_) => {}, - Err(err) => return Err(ErrorCode::SearchError(err.to_string())) + Ok(_) => {} + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), } } Ok(outer_map) } -// The returned item from search is converted to CredentialSearchResult type. +// The returned item from search is converted to CredentialSearchResult type. // If none, a SearchError is returned for no items found. If results found, the "labl" -// key is removed and placed in the outer map's key to differentiate between results. +// key is removed and placed in the outer map's key to differentiate between results. fn to_credential_search_result( item: Option>, - outer_map: &mut HashMap> + outer_map: &mut HashMap>, ) -> Result<()> { let mut result = match item { - None => return Err(ErrorCode::SearchError("Search returned no items".to_string())), - Some(map) => map + None => { + return Err(ErrorCode::SearchError( + "Search returned no items".to_string(), + )) + } + Some(map) => map, }; - let mut formatted: HashMap = HashMap::new(); + let mut formatted: HashMap = HashMap::new(); if result.get_key_value("svce").is_some() { - formatted.insert("Service".to_string(), result.get_key_value("svce").unwrap().1.to_string()); + formatted.insert( + "Service".to_string(), + result.get_key_value("svce").unwrap().1.to_string(), + ); } if result.get_key_value("acct").is_some() { - formatted.insert("Account".to_string(), result.get_key_value("acct").unwrap().1.to_string()); + formatted.insert( + "Account".to_string(), + result.get_key_value("acct").unwrap().1.to_string(), + ); } - let label = result.remove("labl").unwrap_or("EMPTY LABEL".to_string()); + let label = result.remove("labl").unwrap_or("EMPTY LABEL".to_string()); - outer_map.insert(format!("{}", label), formatted); + outer_map.insert(label.to_string(), formatted); Ok(()) } - /// Map a Mac API error to a crate error with appropriate annotation /// /// The MacOS error code values used here are from @@ -339,7 +351,7 @@ mod tests { use security_framework::item; use crate::credential::CredentialPersistence; - use crate::{tests::generate_random_string, Entry, Error, Search, List, Limit}; + use crate::{tests::generate_random_string, Entry, Error, Limit, List, Search}; use super::{default_credential_builder, MacCredential}; use std::collections::HashSet; @@ -423,22 +435,22 @@ mod tests { } fn test_search(by: &str) { - let name = generate_random_string(); - let entry = entry_new(&name, &name); + let name = generate_random_string(); + let entry = entry_new(&name, &name); entry .set_password("test-search-password") - .expect("Failed to set password for test-search"); + .expect("Failed to set password for test-search"); let result = Search::new() .expect("Failed to create new search") - .by(by, &name); + .by(by, &name); let list = List::list_credentials(result, Limit::All) - .expect("Failed to parse HashMap search result"); - let actual: &MacCredential = entry + .expect("Failed to parse HashMap search result"); + let actual: &MacCredential = entry .get_credential() .downcast_ref() .expect("Not a mac credential"); - let mut new_search = item::ItemSearchOptions::new(); + let mut new_search = item::ItemSearchOptions::new(); let search_default = &mut new_search .class(item::ItemClass::generic_password()) @@ -446,29 +458,20 @@ mod tests { .load_attributes(true); let vector_of_results = match by.to_ascii_lowercase().as_str() { - "account" => { - search_default - .account(actual.account.as_str()) - .search() - }, - "service" => { - search_default - .service(actual.account.as_str()) - .search() - }, - "label" => { - search_default - .label(actual.account.as_str()) - .search() - }, - _ => panic!() - }.expect("Failed to get vector of search results in system-framework"); - - let mut expected = String::new(); + "account" => search_default.account(actual.account.as_str()).search(), + "service" => search_default.service(actual.account.as_str()).search(), + "label" => search_default.label(actual.account.as_str()).search(), + _ => panic!(), + } + .expect("Failed to get vector of search results in system-framework"); + + let mut expected = String::new(); for item in vector_of_results { - let mut item = item.simplify_dict().expect("Unable to simplify to dictionary"); - let label = format!("{}\n", &item.remove("labl").expect("No label found")); + let mut item = item + .simplify_dict() + .expect("Unable to simplify to dictionary"); + let label = format!("{}\n", &item.remove("labl").expect("No label found")); let service = format!("\tService:\t{}\n", actual.service); let account = format!("\tAccount:\t{}\n", actual.account); expected.push_str(&label); @@ -476,14 +479,13 @@ mod tests { expected.push_str(&account); } - let expected_set: HashSet<&str> = expected.lines().collect(); - let result_set: HashSet<&str> = list.lines().collect(); + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); assert_eq!(expected_set, result_set, "Search results do not match"); - + entry .delete_password() .expect("Failed to delete mac credential"); - } #[test] @@ -493,12 +495,11 @@ mod tests { #[test] fn test_search_by_label() { - test_search("label") + test_search("label") } #[test] fn test_search_by_account() { test_search("account") } - } diff --git a/src/mock.rs b/src/mock.rs index e8e2e3e..9fedbbf 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -1,363 +1,363 @@ -/*! - -# Mock credential store - -To facilitate testing of clients, this crate provides a Mock credential store -that is platform-independent, provides no persistence, and allows the client -to specify the return values (including errors) for each call. - -To use this credential store instead of the default, make this call during -application startup _before_ creating any entries: -```rust -# use keyring::{set_default_credential_builder, mock}; -set_default_credential_builder(mock::default_credential_builder()); -``` - -You can then create entries as you usually do, and call their usual methods -to set, get, and delete passwords. There is no persistence other than -in the entry itself, so getting a password before setting it will always result -in a [NotFound](Error::NoEntry) error. - -If you want a method call on an entry to fail in a specific way, you can -downcast the entry to a [MockCredential] and then call [set_error](MockCredential::set_error) -with the appropriate error. The next entry method called on the credential -will fail with the error you set. The error will then be cleared, so the next -call on the mock will operate as usual. Here's a complete example: -```rust -# use keyring::{Entry, Error, mock, mock::MockCredential}; -# keyring::set_default_credential_builder(mock::default_credential_builder()); -let entry = Entry::new("service", "user").unwrap(); -let mock: &MockCredential = entry.get_credential().downcast_ref().unwrap(); -mock.set_error(Error::Invalid("mock error".to_string(), "takes precedence".to_string())); -entry.set_password("test").expect_err("error will override"); -entry.set_password("test").expect("error has been cleared"); -``` - */ -use std::cell::RefCell; -use std::sync::Mutex; - -use super::credential::{ - Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, -}; -use super::error::{Error, Result}; - -/// The concrete mock credential -/// -/// Mocks use an internal mutability pattern since entries are read-only. -/// The mutex is used to make sure these are Sync. -#[derive(Debug)] -pub struct MockCredential { - pub inner: Mutex>, -} - -impl Default for MockCredential { - fn default() -> Self { - Self { - inner: Mutex::new(RefCell::new(Default::default())), - } - } -} - -/// The (in-memory) persisted data for a mock credential. -/// -/// We keep a password, but unlike most keystores -/// we also keep an intended error to return on the next call. -/// -/// (Everything about this structure is public for transparency. -/// Most keystore implementation hide their internals.) -#[derive(Debug, Default)] -pub struct MockData { - pub password: Option, - pub error: Option, -} - -impl CredentialApi for MockCredential { - /// Set a password on a mock credential. - /// - /// If there is an error in the mock, it will be returned - /// and the password will _not_ be set. The error will - /// be cleared, so calling again will set the password. - fn set_password(&self, password: &str) -> Result<()> { - let mut inner = self.inner.lock().expect("Can't access mock data for set"); - let data = inner.get_mut(); - let err = data.error.take(); - match err { - None => { - data.password = Some(password.to_string()); - Ok(()) - } - Some(err) => Err(err), - } - } - - /// Get the password from a mock credential, if any. - /// - /// If there is an error set in the mock, it will - /// be returned instead of a password. - fn get_password(&self) -> Result { - let mut inner = self.inner.lock().expect("Can't access mock data for get"); - let data = inner.get_mut(); - let err = data.error.take(); - match err { - None => match &data.password { - None => Err(Error::NoEntry), - Some(val) => Ok(val.clone()), - }, - Some(err) => Err(err), - } - } - - /// Delete the password in a mock credential - /// - /// If there is an error, it will be returned and - /// the deletion will not happen. - /// - /// If there is no password, a [NoEntry](Error::NoEntry) error - /// will be returned. - fn delete_password(&self) -> Result<()> { - let mut inner = self - .inner - .lock() - .expect("Can't access mock data for delete"); - let data = inner.get_mut(); - let err = data.error.take(); - match err { - None => match data.password { - Some(_) => { - data.password = None; - Ok(()) - } - None => Err(Error::NoEntry), - }, - Some(err) => Err(err), - } - } - - /// Return this mock credential concrete object - /// wrapped in the [Any](std::any::Any) trait, - /// so it can be downcast. - fn as_any(&self) -> &dyn std::any::Any { - self - } -} - -impl MockCredential { - /// Make a new mock credential. - /// - /// Since mocks have no persistence between sessions, - /// new mocks always have no password. - fn new_with_target(_target: Option<&str>, _service: &str, _user: &str) -> Result { - Ok(Default::default()) - } - - /// Set an error to be returned from this mock credential. - /// - /// Error returns always take precedence over the normal - /// behavior of the mock. But once an error has been - /// returned it is removed, so the mock works thereafter. - pub fn set_error(&self, err: Error) { - let mut inner = self - .inner - .lock() - .expect("Can't access mock data for set_error"); - let data = inner.get_mut(); - data.error = Some(err); - } -} - -/// The builder for mock credentials. -pub struct MockCredentialBuilder {} - -impl CredentialBuilderApi for MockCredentialBuilder { - /// Build a mock credential for the given target, service, and user. - /// - /// Since mocks don't persist between sessions, all mocks - /// start off without passwords. - fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { - let credential = MockCredential::new_with_target(target, service, user).unwrap(); - Ok(Box::new(credential)) - } - - /// Get an [Any][std::any::Any] reference to the mock credential builder. - fn as_any(&self) -> &dyn std::any::Any { - self - } - - /// This keystore keeps the password in the entry! - fn persistence(&self) -> CredentialPersistence { - CredentialPersistence::EntryOnly - } -} - -/// Return a mock credential builder for use by clients. -pub fn default_credential_builder() -> Box { - Box::new(MockCredentialBuilder {}) -} - -#[cfg(test)] -mod tests { - use super::{default_credential_builder, MockCredential}; - use crate::credential::CredentialPersistence; - use crate::{tests::generate_random_string, Entry, Error}; - - #[test] - fn test_persistence() { - assert!(matches!( - default_credential_builder().persistence(), - CredentialPersistence::EntryOnly - )) - } - - fn entry_new(service: &str, user: &str) -> Entry { - let credential = MockCredential::new_with_target(None, service, user).unwrap(); - Entry::new_with_credential(Box::new(credential)) - } - - #[test] - fn test_missing_entry() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - assert!( - matches!(entry.get_password(), Err(Error::NoEntry)), - "Missing entry has password" - ) - } - - #[test] - fn test_empty_password() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let in_pass = ""; - entry - .set_password(in_pass) - .expect("Can't set empty password"); - let out_pass = entry.get_password().expect("Can't get empty password"); - assert_eq!( - in_pass, out_pass, - "Retrieved and set empty passwords don't match" - ); - entry.delete_password().expect("Can't delete password"); - assert!( - matches!(entry.get_password(), Err(Error::NoEntry)), - "Able to read a deleted password" - ) - } - - #[test] - fn test_round_trip_ascii_password() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "test ascii password"; - entry - .set_password(password) - .expect("Can't set ascii password"); - let stored_password = entry.get_password().expect("Can't get ascii password"); - assert_eq!( - stored_password, password, - "Retrieved and set ascii passwords don't match" - ); - entry - .delete_password() - .expect("Can't delete ascii password"); - assert!( - matches!(entry.get_password(), Err(Error::NoEntry)), - "Able to read a deleted ascii password" - ) - } - - #[test] - fn test_round_trip_non_ascii_password() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "このきれいな花は桜です"; - entry - .set_password(password) - .expect("Can't set non-ascii password"); - let stored_password = entry.get_password().expect("Can't get non-ascii password"); - assert_eq!( - stored_password, password, - "Retrieved and set non-ascii passwords don't match" - ); - entry - .delete_password() - .expect("Can't delete non-ascii password"); - assert!( - matches!(entry.get_password(), Err(Error::NoEntry)), - "Able to read a deleted non-ascii password" - ) - } - - #[test] - fn test_update() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "test ascii password"; - entry - .set_password(password) - .expect("Can't set initial ascii password"); - let stored_password = entry.get_password().expect("Can't get ascii password"); - assert_eq!( - stored_password, password, - "Retrieved and set initial ascii passwords don't match" - ); - let password = "このきれいな花は桜です"; - entry - .set_password(password) - .expect("Can't update ascii with non-ascii password"); - let stored_password = entry.get_password().expect("Can't get non-ascii password"); - assert_eq!( - stored_password, password, - "Retrieved and updated non-ascii passwords don't match" - ); - entry - .delete_password() - .expect("Can't delete updated password"); - assert!( - matches!(entry.get_password(), Err(Error::NoEntry)), - "Able to read a deleted updated password" - ) - } - - #[test] - fn test_set_error() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "test ascii password"; - let mock: &MockCredential = entry - .inner - .as_any() - .downcast_ref() - .expect("Downcast failed"); - mock.set_error(Error::Invalid( - "mock error".to_string(), - "is an error".to_string(), - )); - assert!( - matches!(entry.set_password(password), Err(Error::Invalid(_, _))), - "set: No error" - ); - entry - .set_password(password) - .expect("set: Error not cleared"); - mock.set_error(Error::NoEntry); - assert!( - matches!(entry.get_password(), Err(Error::NoEntry)), - "get: No error" - ); - let stored_password = entry.get_password().expect("get: Error not cleared"); - assert_eq!( - stored_password, password, - "Retrieved and set ascii passwords don't match" - ); - mock.set_error(Error::TooLong("mock".to_string(), 3)); - assert!( - matches!(entry.delete_password(), Err(Error::TooLong(_, 3))), - "delete: No error" - ); - entry.delete_password().expect("delete: Error not cleared"); - assert!( - matches!(entry.get_password(), Err(Error::NoEntry)), - "Able to read a deleted ascii password" - ) - } -} +/*! + +# Mock credential store + +To facilitate testing of clients, this crate provides a Mock credential store +that is platform-independent, provides no persistence, and allows the client +to specify the return values (including errors) for each call. + +To use this credential store instead of the default, make this call during +application startup _before_ creating any entries: +```rust +# use keyring::{set_default_credential_builder, mock}; +set_default_credential_builder(mock::default_credential_builder()); +``` + +You can then create entries as you usually do, and call their usual methods +to set, get, and delete passwords. There is no persistence other than +in the entry itself, so getting a password before setting it will always result +in a [NotFound](Error::NoEntry) error. + +If you want a method call on an entry to fail in a specific way, you can +downcast the entry to a [MockCredential] and then call [set_error](MockCredential::set_error) +with the appropriate error. The next entry method called on the credential +will fail with the error you set. The error will then be cleared, so the next +call on the mock will operate as usual. Here's a complete example: +```rust +# use keyring::{Entry, Error, mock, mock::MockCredential}; +# keyring::set_default_credential_builder(mock::default_credential_builder()); +let entry = Entry::new("service", "user").unwrap(); +let mock: &MockCredential = entry.get_credential().downcast_ref().unwrap(); +mock.set_error(Error::Invalid("mock error".to_string(), "takes precedence".to_string())); +entry.set_password("test").expect_err("error will override"); +entry.set_password("test").expect("error has been cleared"); +``` + */ +use std::cell::RefCell; +use std::sync::Mutex; + +use super::credential::{ + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, +}; +use super::error::{Error, Result}; + +/// The concrete mock credential +/// +/// Mocks use an internal mutability pattern since entries are read-only. +/// The mutex is used to make sure these are Sync. +#[derive(Debug)] +pub struct MockCredential { + pub inner: Mutex>, +} + +impl Default for MockCredential { + fn default() -> Self { + Self { + inner: Mutex::new(RefCell::new(Default::default())), + } + } +} + +/// The (in-memory) persisted data for a mock credential. +/// +/// We keep a password, but unlike most keystores +/// we also keep an intended error to return on the next call. +/// +/// (Everything about this structure is public for transparency. +/// Most keystore implementation hide their internals.) +#[derive(Debug, Default)] +pub struct MockData { + pub password: Option, + pub error: Option, +} + +impl CredentialApi for MockCredential { + /// Set a password on a mock credential. + /// + /// If there is an error in the mock, it will be returned + /// and the password will _not_ be set. The error will + /// be cleared, so calling again will set the password. + fn set_password(&self, password: &str) -> Result<()> { + let mut inner = self.inner.lock().expect("Can't access mock data for set"); + let data = inner.get_mut(); + let err = data.error.take(); + match err { + None => { + data.password = Some(password.to_string()); + Ok(()) + } + Some(err) => Err(err), + } + } + + /// Get the password from a mock credential, if any. + /// + /// If there is an error set in the mock, it will + /// be returned instead of a password. + fn get_password(&self) -> Result { + let mut inner = self.inner.lock().expect("Can't access mock data for get"); + let data = inner.get_mut(); + let err = data.error.take(); + match err { + None => match &data.password { + None => Err(Error::NoEntry), + Some(val) => Ok(val.clone()), + }, + Some(err) => Err(err), + } + } + + /// Delete the password in a mock credential + /// + /// If there is an error, it will be returned and + /// the deletion will not happen. + /// + /// If there is no password, a [NoEntry](Error::NoEntry) error + /// will be returned. + fn delete_password(&self) -> Result<()> { + let mut inner = self + .inner + .lock() + .expect("Can't access mock data for delete"); + let data = inner.get_mut(); + let err = data.error.take(); + match err { + None => match data.password { + Some(_) => { + data.password = None; + Ok(()) + } + None => Err(Error::NoEntry), + }, + Some(err) => Err(err), + } + } + + /// Return this mock credential concrete object + /// wrapped in the [Any](std::any::Any) trait, + /// so it can be downcast. + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl MockCredential { + /// Make a new mock credential. + /// + /// Since mocks have no persistence between sessions, + /// new mocks always have no password. + fn new_with_target(_target: Option<&str>, _service: &str, _user: &str) -> Result { + Ok(Default::default()) + } + + /// Set an error to be returned from this mock credential. + /// + /// Error returns always take precedence over the normal + /// behavior of the mock. But once an error has been + /// returned it is removed, so the mock works thereafter. + pub fn set_error(&self, err: Error) { + let mut inner = self + .inner + .lock() + .expect("Can't access mock data for set_error"); + let data = inner.get_mut(); + data.error = Some(err); + } +} + +/// The builder for mock credentials. +pub struct MockCredentialBuilder {} + +impl CredentialBuilderApi for MockCredentialBuilder { + /// Build a mock credential for the given target, service, and user. + /// + /// Since mocks don't persist between sessions, all mocks + /// start off without passwords. + fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { + let credential = MockCredential::new_with_target(target, service, user).unwrap(); + Ok(Box::new(credential)) + } + + /// Get an [Any][std::any::Any] reference to the mock credential builder. + fn as_any(&self) -> &dyn std::any::Any { + self + } + + /// This keystore keeps the password in the entry! + fn persistence(&self) -> CredentialPersistence { + CredentialPersistence::EntryOnly + } +} + +/// Return a mock credential builder for use by clients. +pub fn default_credential_builder() -> Box { + Box::new(MockCredentialBuilder {}) +} + +#[cfg(test)] +mod tests { + use super::{default_credential_builder, MockCredential}; + use crate::credential::CredentialPersistence; + use crate::{tests::generate_random_string, Entry, Error}; + + #[test] + fn test_persistence() { + assert!(matches!( + default_credential_builder().persistence(), + CredentialPersistence::EntryOnly + )) + } + + fn entry_new(service: &str, user: &str) -> Entry { + let credential = MockCredential::new_with_target(None, service, user).unwrap(); + Entry::new_with_credential(Box::new(credential)) + } + + #[test] + fn test_missing_entry() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + assert!( + matches!(entry.get_password(), Err(Error::NoEntry)), + "Missing entry has password" + ) + } + + #[test] + fn test_empty_password() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let in_pass = ""; + entry + .set_password(in_pass) + .expect("Can't set empty password"); + let out_pass = entry.get_password().expect("Can't get empty password"); + assert_eq!( + in_pass, out_pass, + "Retrieved and set empty passwords don't match" + ); + entry.delete_password().expect("Can't delete password"); + assert!( + matches!(entry.get_password(), Err(Error::NoEntry)), + "Able to read a deleted password" + ) + } + + #[test] + fn test_round_trip_ascii_password() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "test ascii password"; + entry + .set_password(password) + .expect("Can't set ascii password"); + let stored_password = entry.get_password().expect("Can't get ascii password"); + assert_eq!( + stored_password, password, + "Retrieved and set ascii passwords don't match" + ); + entry + .delete_password() + .expect("Can't delete ascii password"); + assert!( + matches!(entry.get_password(), Err(Error::NoEntry)), + "Able to read a deleted ascii password" + ) + } + + #[test] + fn test_round_trip_non_ascii_password() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "このきれいな花は桜です"; + entry + .set_password(password) + .expect("Can't set non-ascii password"); + let stored_password = entry.get_password().expect("Can't get non-ascii password"); + assert_eq!( + stored_password, password, + "Retrieved and set non-ascii passwords don't match" + ); + entry + .delete_password() + .expect("Can't delete non-ascii password"); + assert!( + matches!(entry.get_password(), Err(Error::NoEntry)), + "Able to read a deleted non-ascii password" + ) + } + + #[test] + fn test_update() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "test ascii password"; + entry + .set_password(password) + .expect("Can't set initial ascii password"); + let stored_password = entry.get_password().expect("Can't get ascii password"); + assert_eq!( + stored_password, password, + "Retrieved and set initial ascii passwords don't match" + ); + let password = "このきれいな花は桜です"; + entry + .set_password(password) + .expect("Can't update ascii with non-ascii password"); + let stored_password = entry.get_password().expect("Can't get non-ascii password"); + assert_eq!( + stored_password, password, + "Retrieved and updated non-ascii passwords don't match" + ); + entry + .delete_password() + .expect("Can't delete updated password"); + assert!( + matches!(entry.get_password(), Err(Error::NoEntry)), + "Able to read a deleted updated password" + ) + } + + #[test] + fn test_set_error() { + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "test ascii password"; + let mock: &MockCredential = entry + .inner + .as_any() + .downcast_ref() + .expect("Downcast failed"); + mock.set_error(Error::Invalid( + "mock error".to_string(), + "is an error".to_string(), + )); + assert!( + matches!(entry.set_password(password), Err(Error::Invalid(_, _))), + "set: No error" + ); + entry + .set_password(password) + .expect("set: Error not cleared"); + mock.set_error(Error::NoEntry); + assert!( + matches!(entry.get_password(), Err(Error::NoEntry)), + "get: No error" + ); + let stored_password = entry.get_password().expect("get: Error not cleared"); + assert_eq!( + stored_password, password, + "Retrieved and set ascii passwords don't match" + ); + mock.set_error(Error::TooLong("mock".to_string(), 3)); + assert!( + matches!(entry.delete_password(), Err(Error::TooLong(_, 3))), + "delete: No error" + ); + entry.delete_password().expect("delete: Error not cleared"); + assert!( + matches!(entry.get_password(), Err(Error::NoEntry)), + "Able to read a deleted ascii password" + ) + } +} diff --git a/src/secret_service.rs b/src/secret_service.rs index 4d4b389..8b551b7 100644 --- a/src/secret_service.rs +++ b/src/secret_service.rs @@ -83,8 +83,8 @@ use secret_service::blocking::{Collection, Item, SecretService}; use secret_service::{EncryptionType, Error}; use super::credential::{ - Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, - CredentialSearch, CredentialSearchApi, CredentialSearchResult + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialSearch, + CredentialSearchApi, CredentialSearchResult, }; use super::error::{decode_password, Error as ErrorCode, Result}; @@ -330,11 +330,11 @@ impl CredentialBuilderApi for SsCredentialBuilder { } } -pub struct SsCredentialSearch {} +pub struct SsCredentialSearch {} -/// Returns the Secret service default credential search structure. +/// Returns the Secret service default credential search structure. /// -/// This creates a new search structure. The by method has no concrete search types +/// This creates a new search structure. The by method has no concrete search types /// like in Windows, iOS, and MacOS. The keys to these credentials can be whatever the user sets them to /// and is displayed as a HashMap. pub fn default_credential_search() -> Box { @@ -352,22 +352,22 @@ fn search_items(by: &str, query: &str) -> CredentialSearchResult { let ss = match SecretService::connect(EncryptionType::Plain) { Ok(connection) => connection, Err(err) => return Err(ErrorCode::SearchError(err.to_string())), - }; + }; let collections = match ss.get_all_collections() { Ok(collections) => collections, Err(err) => return Err(ErrorCode::SearchError(err.to_string())), - }; + }; let mut search_map = HashMap::new(); - search_map.insert(by, query); + search_map.insert(by, query); - let mut outer_map: HashMap> = HashMap::new(); + let mut outer_map: HashMap> = HashMap::new(); for collection in collections { let search_results = match collection.search_items(search_map.clone()) { - Ok(results) => results, + Ok(results) => results, Err(err) => return Err(ErrorCode::SearchError(err.to_string())), - }; + }; for result in search_results { let attributes = match result.get_attributes() { @@ -378,7 +378,6 @@ fn search_items(by: &str, query: &str) -> CredentialSearchResult { let mut inner_map: HashMap = HashMap::new(); for (key, value) in attributes { - inner_map.insert(key, value); let label = match result.get_label() { @@ -386,10 +385,10 @@ fn search_items(by: &str, query: &str) -> CredentialSearchResult { Err(err) => return Err(ErrorCode::SearchError(err.to_string())), }; - outer_map.insert(label.clone(), inner_map.clone()); + outer_map.insert(label.clone(), inner_map.clone()); } } - }; + } Ok(outer_map) } @@ -508,11 +507,11 @@ fn wrap(err: Error) -> Box { #[cfg(test)] mod tests { use crate::credential::CredentialPersistence; - use crate::{tests::generate_random_string, Entry, Error, Search, List, Limit}; + use crate::{tests::generate_random_string, Entry, Error, Limit, List, Search}; use super::{default_credential_builder, SsCredential}; - use std::collections::HashSet; + use std::collections::HashSet; #[test] fn test_persistence() { @@ -742,12 +741,12 @@ mod tests { #[test] fn test_search() { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "search test password"; + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "search test password"; entry .set_password(password) - .expect("Not a Secret Service credential"); + .expect("Not a Secret Service credential"); let result = Search::new() .expect("Failed to build search") .by("service", &name); @@ -757,16 +756,16 @@ mod tests { let actual: &SsCredential = entry .get_credential() .downcast_ref() - .expect("Not a Secret Service credential"); + .expect("Not a Secret Service credential"); - let mut expected = format!("{}\n", actual.label); - let attributes = &actual.attributes; + let mut expected = format!("{}\n", actual.label); + let attributes = &actual.attributes; for (key, value) in attributes { let attribute = format!("\t{}:\t{}\n", key, value); expected.push_str(attribute.as_str()); } - let expected_set: HashSet<&str> = expected.lines().collect(); - let result_set: HashSet<&str> = list.lines().collect(); + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); assert_eq!(expected_set, result_set, "Search results do not match"); entry .delete_password() diff --git a/src/windows.rs b/src/windows.rs index b05868c..e057776 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -40,15 +40,15 @@ use windows_sys::Win32::Foundation::{ ERROR_NOT_FOUND, ERROR_NO_SUCH_LOGON_SESSION, FILETIME, }; use windows_sys::Win32::Security::Credentials::{ - CredDeleteW, CredEnumerateW, CredFree, CredReadW, CredWriteW, - CREDENTIALW, CREDENTIAL_ATTRIBUTEW, CRED_ENUMERATE_ALL_CREDENTIALS, CRED_FLAGS, + CredDeleteW, CredEnumerateW, CredFree, CredReadW, CredWriteW, CREDENTIALW, + CREDENTIAL_ATTRIBUTEW, CRED_ENUMERATE_ALL_CREDENTIALS, CRED_FLAGS, CRED_MAX_CREDENTIAL_BLOB_SIZE, CRED_MAX_GENERIC_TARGET_NAME_LENGTH, CRED_MAX_STRING_LENGTH, CRED_MAX_USERNAME_LENGTH, CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC, }; use super::credential::{ - Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, - CredentialSearch, CredentialSearchApi, CredentialSearchResult + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialSearch, + CredentialSearchApi, CredentialSearchResult, }; use super::error::{Error as ErrorCode, Result}; @@ -333,76 +333,76 @@ pub struct WinCredentialSearch {} /// Returns an instance of the Windows credential search. /// /// Can be specified to search by certain credential parameters -/// and by a query parameter. +/// and by a query parameter. pub fn default_credential_search() -> Box { Box::new(WinCredentialSearch {}) } impl CredentialSearchApi for WinCredentialSearch { /// Specifies what parameter to search by and the query string - /// + /// /// Can return a [SearchError](Error::SearchError) /// # Example /// let search = keyring::Search::new().unwrap(); /// let results = search.by("user", "Mr. Foo Bar"); fn by(&self, by: &str, query: &str) -> CredentialSearchResult { let results = match search_type(by, query) { - Ok(results) => results, - Err(err) => return Err(ErrorCode::SearchError(err.to_string())) - }; + Ok(results) => results, + Err(err) => return Err(ErrorCode::SearchError(err.to_string())), + }; - let mut outer_map: HashMap> = HashMap::new(); + let mut outer_map: HashMap> = HashMap::new(); for result in results { - let mut inner_map: HashMap = HashMap::new(); - + let mut inner_map: HashMap = HashMap::new(); + inner_map.insert("Service".to_string(), result.comment); - inner_map.insert("User".to_string(), result.username); - - outer_map.insert(format!("{}", result.target_name), inner_map); + inner_map.insert("User".to_string(), result.username); + + outer_map.insert(result.target_name.to_string(), inner_map); } - + Ok(outer_map) } - } // Type matching for search types enum WinSearchType { Target, - Service, - User + Service, + User, } -// Match search type -fn search_type(by: &str, query: &str) -> Result>> { +// Match search type +fn search_type(by: &str, query: &str) -> Result> { let search_type = match by.to_ascii_lowercase().as_str() { - "target" => { WinSearchType::Target }, - "service" => { WinSearchType::Service }, - "user" => { WinSearchType::User } - _ => { return Err(ErrorCode::SearchError("Invalid search parameter, not Target, Service, or User".to_string())) } + "target" => WinSearchType::Target, + "service" => WinSearchType::Service, + "user" => WinSearchType::User, + _ => { + return Err(ErrorCode::SearchError( + "Invalid search parameter, not Target, Service, or User".to_string(), + )) + } }; - search(&search_type, &query) - + search(&search_type, query) } // Perform search, can return a regex error if the search parameter is invalid -fn search(search_type: &WinSearchType, search_parameter: &str) -> Result>> { +fn search(search_type: &WinSearchType, search_parameter: &str) -> Result> { let credentials = get_all_credentials(); - let re = format!(r#"(?i){}"#, search_parameter); + let re = format!(r#"(?i){}"#, search_parameter); let regex = match Regex::new(re.as_str()) { Ok(regex) => regex, - Err(err) => return Err(ErrorCode::SearchError( - format!("Regex Error, {}", err) - )) + Err(err) => return Err(ErrorCode::SearchError(format!("Regex Error, {}", err))), }; - - let mut results = Vec::new(); + + let mut results = Vec::new(); for credential in credentials { let haystack = match search_type { WinSearchType::Target => &credential.target_name, WinSearchType::Service => &credential.comment, - WinSearchType::User => &credential.username + WinSearchType::User => &credential.username, }; if regex.is_match(haystack) { results.push(credential); @@ -413,17 +413,17 @@ fn search(search_type: &WinSearchType, search_parameter: &str) -> Result Vec> { - let mut entries: Vec> = Vec::new(); +fn get_all_credentials() -> Vec { + let mut entries: Vec = Vec::new(); let mut count = 0; let mut credentials_ptr = std::ptr::null_mut(); - + unsafe { CredEnumerateW( std::ptr::null(), @@ -432,40 +432,36 @@ fn get_all_credentials() -> Vec> { &mut credentials_ptr, ); } - + let credentials = unsafe { std::slice::from_raw_parts::<&CREDENTIALW>(credentials_ptr as _, count as usize) }; - - for credential in credentials { + + for credential in credentials { let target_name = unsafe { from_wstr(credential.TargetName) }; // By default the target names are prepended with the credential type // i.e. LegacyGeneric:target=Example Target Name. This is where // The '=' is indexed to strip the prepended type - let index = match target_name.find('=') { - Some(index) => index, - None => 0 - }; - let target_name = target_name[ index + 1.. ].to_string(); + let index = target_name.find('=').unwrap_or(0); + let target_name = target_name[index + 1..].to_string(); - let username; - if (unsafe { from_wstr(credential.UserName) } == "") { - username = String::from("NO USER"); + let username = if unsafe { from_wstr(credential.UserName) }.is_empty() { + String::from("NO USER") } else { - username = unsafe { from_wstr(credential.UserName) }; - } - let target_alias = unsafe { from_wstr(credential.TargetAlias) }; - let comment = unsafe { from_wstr(credential.Comment) }; + unsafe { from_wstr(credential.UserName) } + }; + let target_alias = unsafe { from_wstr(credential.TargetAlias) }; + let comment = unsafe { from_wstr(credential.Comment) }; - entries.push( Box::new(WinCredential { + entries.push(WinCredential { username, target_name, target_alias, - comment - })); - }; - + comment, + }); + } + unsafe { CredFree(std::mem::transmute(credentials_ptr)) }; - + entries } @@ -556,9 +552,9 @@ mod tests { use crate::credential::CredentialPersistence; use crate::tests::{generate_random_string, generate_random_string_of_len}; - use crate::{Entry, Search, List, Limit}; + use crate::{Entry, Limit, List, Search}; - use std::collections::HashSet; + use std::collections::HashSet; #[test] fn test_persistence() { @@ -747,26 +743,27 @@ mod tests { } fn test_search(by: &str) { - let name = generate_random_string(); - let entry = entry_new(&name, &name); - let password = "search test password"; + let name = generate_random_string(); + let entry = entry_new(&name, &name); + let password = "search test password"; entry .set_password(password) - .expect("Not a windows credential"); - let result = Search::new() - .expect("Failed to build search") - .by(by, &name); + .expect("Not a windows credential"); + let result = Search::new().expect("Failed to build search").by(by, &name); let list = List::list_credentials(result, Limit::All) .expect("Failed to parse string from HashMap result"); let actual: &WinCredential = entry .get_credential() .downcast_ref() - .expect("Not a windows credential"); + .expect("Not a windows credential"); - let expected = format!("{}\n\tService:\t{}\n\tUser:\t{}\n", actual.target_name, actual.comment, actual.username); - let expected_set: HashSet<&str> = expected.lines().collect(); - let result_set: HashSet<&str> = list.lines().collect(); + let expected = format!( + "{}\n\tService:\t{}\n\tUser:\t{}\n", + actual.target_name, actual.comment, actual.username + ); + let expected_set: HashSet<&str> = expected.lines().collect(); + let result_set: HashSet<&str> = list.lines().collect(); assert_eq!(expected_set, result_set, "Search results do not match"); entry .delete_password() From a168fb173b3cc2aa90c32492637eacf8dc7a96ff Mon Sep 17 00:00:00 2001 From: Nick Wimmers Date: Thu, 2 May 2024 23:42:34 -0400 Subject: [PATCH 14/14] keyutils passing test fmt --- src/keyutils.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/keyutils.rs b/src/keyutils.rs index a2d8b56..369c74d 100644 --- a/src/keyutils.rs +++ b/src/keyutils.rs @@ -524,9 +524,10 @@ mod tests { .set_password(password) .expect("Not a keyutils credential"); let query = format!("keyring-rs:{}@{}", name, name); - let result = Search::new() - .expect("Failed to build search") - .by("session", &query); + let result = Search { + inner: Box::new(super::KeyutilsCredentialSearch {}), + } + .by("session", &query); let list = List::list_credentials(result, Limit::All) .expect("Failed to parse string from HashMap result");