keyring_search/
lib.rs

1/*!
2
3# Keyring
4
5This is a cross-platform library for searching the platform specific keystore.
6[Crates.io](https://crates.io/crates/keyring-search).
7Currently supported platforms are
8Linux,
9Windows,
10macOS, and iOS.
11
12## Design
13
14This crate, originally planned as a feature for
15[keyring](https://crates.io/crates/keyring) provides a broad search of
16the platform specific keystores based on user provided search parameters.
17 */
18
19use std::collections::HashMap;
20
21pub use error::{Error, Result};
22pub use search::{CredentialSearch, CredentialSearchResult, Limit};
23// Included keystore implementations and default choice thereof.
24
25pub mod mock;
26
27#[cfg(all(target_os = "linux", feature = "linux-keyutils"))]
28pub mod keyutils;
29#[cfg(all(
30    target_os = "linux",
31    feature = "secret-service",
32    not(feature = "linux-no-secret-service")
33))]
34pub mod secret_service;
35#[cfg(all(
36    target_os = "linux",
37    feature = "secret-service",
38    not(feature = "linux-default-keyutils")
39))]
40use crate::secret_service as default;
41#[cfg(all(
42    target_os = "linux",
43    feature = "linux-keyutils",
44    any(feature = "linux-default-keyutils", not(feature = "secret-service"))
45))]
46use keyutils as default;
47#[cfg(all(
48    target_os = "linux",
49    not(feature = "secret-service"),
50    not(feature = "linux-keyutils")
51))]
52use mock as default;
53
54#[cfg(all(target_os = "freebsd", feature = "secret-service"))]
55pub mod secret_service;
56#[cfg(all(target_os = "freebsd", feature = "secret-service"))]
57use crate::secret_service as default;
58#[cfg(all(target_os = "freebsd", not(feature = "secret-service")))]
59use mock as default;
60
61#[cfg(all(target_os = "openbsd", feature = "secret-service"))]
62pub mod secret_service;
63#[cfg(all(target_os = "openbsd", feature = "secret-service"))]
64use crate::secret_service as default;
65#[cfg(all(target_os = "openbsd", not(feature = "secret-service")))]
66use mock as default;
67
68#[cfg(all(target_os = "macos", feature = "platform-macos"))]
69pub mod macos;
70#[cfg(all(target_os = "macos", feature = "platform-macos"))]
71use macos as default;
72#[cfg(all(target_os = "macos", not(feature = "platform-macos")))]
73use mock as default;
74
75#[cfg(all(target_os = "windows", feature = "platform-windows"))]
76pub mod windows;
77#[cfg(all(target_os = "windows", not(feature = "platform-windows")))]
78use mock as default;
79#[cfg(all(target_os = "windows", feature = "platform-windows"))]
80use windows as default;
81
82#[cfg(all(target_os = "ios", feature = "platform-ios"))]
83pub mod ios;
84#[cfg(all(target_os = "ios", feature = "platform-ios"))]
85use ios as default;
86#[cfg(all(target_os = "ios", not(feature = "platform-ios")))]
87use mock as default;
88
89#[cfg(not(any(
90    target_os = "linux",
91    target_os = "freebsd",
92    target_os = "openbsd",
93    target_os = "macos",
94    target_os = "ios",
95    target_os = "windows",
96)))]
97use mock as default;
98
99pub mod error;
100pub mod search;
101
102pub fn set_default_credential_search(default_search: Box<CredentialSearch>) -> Result<Search> {
103    Ok(Search {
104        inner: default_search,
105    })
106}
107
108fn default_credential_search() -> Result<Search> {
109    let credentials = default::default_credential_search();
110    Ok(Search { inner: credentials })
111}
112
113pub struct Search {
114    inner: Box<CredentialSearch>,
115}
116/// The implementation of the Search structures methods.
117///
118/// The default search types are: Target, User, and Service.
119/// On linux-keyutils these all default to searching the 'session'
120/// keyring. If searching in a different keyring, utilize the
121/// platform specific `search_by_keyring` function
122impl Search {
123    /// Create a new instance of the Credential Search.
124    ///
125    /// The default credential search is used.
126    pub fn new() -> Result<Search> {
127        default_credential_search()
128    }
129    /// Specifies searching by target and the query string
130    ///
131    /// Can return:
132    /// [SearchError](Error::SearchError)
133    /// [NoResults](Error::NoResults)
134    /// [Unexpected](Error::Unexpected)
135    ///
136    /// # Example
137    ///     let search = keyring_search::Search::new().unwrap();
138    ///     let results = search.by_target("Foo.app");
139    pub fn by_target(&self, query: &str) -> CredentialSearchResult {
140        self.inner.by("target", query)
141    }
142    /// Specifies searching by user and the query string
143    ///
144    /// Can return:
145    /// [SearchError](Error::SearchError)
146    /// [NoResults](Error::NoResults)
147    /// [Unexpected](Error::Unexpected)
148    ///
149    /// # Example
150    ///     let search = keyring_search::Search::new().unwrap();
151    ///     let results = search.by_user("Mr. Foo Bar");
152    pub fn by_user(&self, query: &str) -> CredentialSearchResult {
153        self.inner.by("user", query)
154    }
155    /// Specifies searching by service and the query string
156    ///
157    /// Can return:
158    /// [SearchError](Error::SearchError)
159    /// [NoResults](Error::NoResults)
160    /// [Unexpected](Error::Unexpected)
161    ///
162    /// # Example
163    ///     let search = keyring_search::Search::new().unwrap();
164    ///     let results = search.by_service("Bar inc.");
165    pub fn by_service(&self, query: &str) -> CredentialSearchResult {
166        self.inner.by("service", query)
167    }
168}
169
170pub struct List {}
171
172/// Implementation of methods for the `List` structure.
173///
174/// `list_all`, lists all returned credentials
175/// `list_max`, lists a specified max amount of
176/// credentials. These are specified by calling [list_credentials](List::list_credentials).
177///
178/// Linux-keyutils search feature is limited to one result,
179/// no matter the `Limit`, one result will be returned.
180impl List {
181    /// List the credentials with given search result
182    ///
183    /// Takes CredentialSearchResult type and converts to a string
184    /// for printing. Matches the Limit type passed to constrain
185    /// the amount of results added to the string
186    pub fn list_credentials(search_result: &CredentialSearchResult, limit: Limit) -> String {
187        match limit {
188            Limit::All => Self::list_all(search_result),
189            Limit::Max(max) => Self::list_max(search_result, max),
190        }
191    }
192    /// List all credential search results.
193    ///
194    /// Is the result of passing the Limit::All type
195    /// to list_credentials.
196    fn list_all(result: &CredentialSearchResult) -> String {
197        let mut output = String::new();
198        match result {
199            Ok(search_result) => {
200                let mut entries: Vec<(String, HashMap<String, String>)> = search_result
201                    .iter()
202                    .map(|(k, v)| (k.clone(), v.clone()))
203                    .collect();
204                entries.sort_by_key(|(k, _)| k.parse::<i32>().unwrap_or(0));
205
206                for (outer_key, inner_map) in entries {
207                    output.push_str(&format!("{}\n", outer_key));
208                    let mut metadata: Vec<(String, String)> = inner_map
209                        .iter()
210                        .map(|(k, v)| (k.clone(), v.clone()))
211                        .collect();
212                    metadata.sort_by(|a, b| a.0.cmp(&b.0));
213                    for (key, value) in metadata {
214                        output.push_str(&format!("{}: {}\n", key, value));
215                    }
216                }
217                println!("Search returned {} results\n", search_result.keys().len());
218                output
219            }
220            Err(err) => err.to_string(),
221        }
222    }
223    /// List a certain amount of credential search results.
224    ///
225    /// Is the result of passing the Limit::Max(i64) type
226    /// to list_credentials. The 64 bit integer represents
227    /// the total of the results passed.
228    fn list_max(result: &CredentialSearchResult, max: i64) -> String {
229        let mut output = String::new();
230        let mut count = 1;
231        match result {
232            Ok(search_result) => {
233                let mut entries: Vec<(String, HashMap<String, String>)> = search_result
234                    .iter()
235                    .map(|(k, v)| (k.clone(), v.clone()))
236                    .collect();
237                entries.sort_by_key(|(k, _)| k.parse::<i32>().unwrap_or(0));
238
239                for (outer_key, inner_map) in entries {
240                    output.push_str(&format!("{}\n", outer_key));
241                    let mut metadata: Vec<(String, String)> = inner_map
242                        .iter()
243                        .map(|(k, v)| (k.clone(), v.clone()))
244                        .collect();
245                    metadata.sort_by(|a, b| a.0.cmp(&b.0));
246                    for (key, value) in metadata {
247                        output.push_str(&format!("{}: {}\n", key, value));
248                    }
249                    count += 1;
250                    if count > max {
251                        break;
252                    }
253                }
254                println!("Search returned {} results\n", search_result.keys().len());
255                output
256            }
257            Err(err) => err.to_string(),
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    pub fn generate_random_string_of_len(len: usize) -> String {
265        // from the Rust Cookbook:
266        // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html
267        use rand::{distributions::Alphanumeric, thread_rng, Rng};
268        thread_rng()
269            .sample_iter(&Alphanumeric)
270            .take(len)
271            .map(char::from)
272            .collect()
273    }
274
275    pub fn generate_random_string() -> String {
276        generate_random_string_of_len(30)
277    }
278}