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()