secret_service/lib.rs
1//! # Secret Service libary
2//!
3//! This library implements a rust interface to the Secret Service API which is implemented
4//! in Linux.
5//!
6//! ## About Secret Service API
7//! <https://standards.freedesktop.org/secret-service/>
8//!
9//! Secret Service provides a secure place to store secrets.
10//! Gnome keyring and KWallet implement the Secret Service API.
11//!
12//! ## Basic Usage
13//! ```
14//! use secret_service::SecretService;
15//! use secret_service::EncryptionType;
16//! use std::collections::HashMap;
17//!
18//! #[tokio::main(flavor = "current_thread")]
19//! async fn main() {
20//! // initialize secret service (dbus connection and encryption session)
21//! let ss = SecretService::connect(EncryptionType::Dh).await.unwrap();
22//!
23//! // get default collection
24//! let collection = ss.get_default_collection().await.unwrap();
25//!
26//! let mut properties = HashMap::new();
27//! properties.insert("test", "test_value");
28//!
29//! //create new item
30//! collection.create_item(
31//! "test_label", // label
32//! properties,
33//! b"test_secret", //secret
34//! false, // replace item with same attributes
35//! "text/plain" // secret content type
36//! ).await.unwrap();
37//!
38//! // search items by properties
39//! let search_items = ss.search_items(
40//! HashMap::from([("test", "test_value")])
41//! ).await.unwrap();
42//!
43//! // retrieve one item, first by checking the unlocked items
44//! let item = match search_items.unlocked.first() {
45//! Some(item) => item,
46//! None => {
47//! // if there aren't any, check the locked items and unlock the first one
48//! let locked_item = search_items
49//! .locked
50//! .first()
51//! .expect("Search didn't return any items!");
52//! locked_item.unlock().await.unwrap();
53//! locked_item
54//! }
55//! };
56//!
57//! // retrieve secret from item
58//! let secret = item.get_secret().await.unwrap();
59//! assert_eq!(secret, b"test_secret");
60//!
61//! // delete item (deletes the dbus object, not the struct instance)
62//! item.delete().await.unwrap()
63//! }
64//! ```
65//!
66//! ## Overview of this library:
67//! ### Entry point
68//! The entry point for this library is the `SecretService` struct. A new instance of
69//! `SecretService` will initialize the dbus connection and negotiate an encryption session.
70//!
71//! ```
72//! # use secret_service::SecretService;
73//! # use secret_service::EncryptionType;
74//! # async fn call() {
75//! SecretService::connect(EncryptionType::Plain).await.unwrap();
76//! # }
77//! ```
78//!
79//! or
80//!
81//! ```
82//! # use secret_service::SecretService;
83//! # use secret_service::EncryptionType;
84//! # async fn call() {
85//! SecretService::connect(EncryptionType::Dh).await.unwrap();
86//! # }
87//! ```
88//!
89//! Once the SecretService struct is initialized, it can be used to navigate to a collection.
90//! Items can also be directly searched for without getting a collection first.
91//!
92//! ### Collections and Items
93//! The Secret Service API organizes secrets into collections, and holds each secret
94//! in an item.
95//!
96//! Items consist of a label, attributes, and the secret. The most common way to find
97//! an item is a search by attributes.
98//!
99//! While it's possible to create new collections, most users will simply create items
100//! within the default collection.
101//!
102//! ### Actions overview
103//! The most common supported actions are `create`, `get`, `search`, and `delete` for
104//! `Collections` and `Items`. For more specifics and exact method names, please see
105//! each struct's documentation.
106//!
107//! In addition, `set` and `get` actions are available for secrets contained in an `Item`.
108//!
109//! ### Crypto
110//! Specifics in SecretService API Draft Proposal:
111//! <https://standards.freedesktop.org/secret-service/>
112//!
113//! ### Async
114//!
115//! This crate, following `zbus`, is async by default. If you want a synchronous interface
116//! that blocks, see the [blocking] module instead.
117//
118// Util currently has interfaces (dbus method namespace) to make it easier to call methods.
119// Util contains function to execute prompts (used in many collection and item methods, like
120// delete)
121
122pub mod blocking;
123mod error;
124mod proxy;
125mod session;
126mod ss;
127mod util;
128
129mod collection;
130pub use collection::Collection;
131
132pub use error::Error;
133
134mod item;
135pub use item::Item;
136
137pub use session::EncryptionType;
138
139use crate::proxy::service::ServiceProxy;
140use crate::session::Session;
141use crate::ss::SS_COLLECTION_LABEL;
142use crate::util::exec_prompt;
143use futures_util::TryFutureExt;
144use std::collections::HashMap;
145use zbus::zvariant::{ObjectPath, OwnedObjectPath, Value};
146
147/// Secret Service Struct.
148///
149/// This the main entry point for usage of the library.
150///
151/// Creating a new [SecretService] will also initialize dbus
152/// and negotiate a new cryptographic session
153/// ([EncryptionType::Plain] or [EncryptionType::Dh])
154pub struct SecretService<'a> {
155 conn: zbus::Connection,
156 session: Session,
157 service_proxy: ServiceProxy<'a>,
158}
159
160/// Used to indicate locked and unlocked items in the
161/// return value of [SecretService::search_items]
162/// and [blocking::SecretService::search_items].
163pub struct SearchItemsResult<T> {
164 pub unlocked: Vec<T>,
165 pub locked: Vec<T>,
166}
167
168impl<'a> SecretService<'a> {
169 /// Create a new [SecretService] instance.
170 ///
171 /// This will initialize its own connection to the session bus.
172 pub async fn connect(encryption: EncryptionType) -> Result<SecretService<'a>, Error> {
173 let conn = zbus::Connection::session()
174 .await
175 .map_err(util::handle_conn_error)?;
176
177 Self::connect_with_existing(encryption, conn).await
178 }
179
180 /// Creates a new [SecretService] instance, utilizing an existing connection handle.
181 ///
182 /// `session_conn` should be connected to the session/user message bus.
183 pub async fn connect_with_existing(
184 encryption: EncryptionType,
185 session_conn: zbus::Connection,
186 ) -> Result<SecretService<'a>, Error> {
187 let service_proxy = ServiceProxy::new(&session_conn)
188 .await
189 .map_err(util::handle_conn_error)?;
190
191 let session = Session::new(&service_proxy, encryption).await?;
192
193 Ok(SecretService {
194 conn: session_conn,
195 session,
196 service_proxy,
197 })
198 }
199
200 /// Get all collections
201 pub async fn get_all_collections(&self) -> Result<Vec<Collection<'_>>, Error> {
202 let collections = self.service_proxy.collections().await?;
203
204 futures_util::future::join_all(collections.into_iter().map(|object_path| {
205 Collection::new(
206 self.conn.clone(),
207 &self.session,
208 &self.service_proxy,
209 object_path.into(),
210 )
211 }))
212 .await
213 .into_iter()
214 .collect::<Result<_, _>>()
215 }
216
217 /// Get collection by alias.
218 ///
219 /// Most common would be the `default` alias, but there
220 /// is also a specific method for getting the collection
221 /// by default alias.
222 pub async fn get_collection_by_alias(&self, alias: &str) -> Result<Collection<'_>, Error> {
223 let object_path = self.service_proxy.read_alias(alias).await?;
224
225 if object_path.as_str() == "/" {
226 Err(Error::NoResult)
227 } else {
228 Collection::new(
229 self.conn.clone(),
230 &self.session,
231 &self.service_proxy,
232 object_path,
233 )
234 .await
235 }
236 }
237
238 /// Get default collection.
239 /// (The collection whos alias is `default`)
240 pub async fn get_default_collection(&self) -> Result<Collection<'_>, Error> {
241 self.get_collection_by_alias("default").await
242 }
243
244 /// Get any collection.
245 /// First tries `default` collection, then `session`
246 /// collection, then the first collection when it
247 /// gets all collections.
248 pub async fn get_any_collection(&self) -> Result<Collection<'_>, Error> {
249 // default first, then session, then first
250
251 self.get_default_collection()
252 .or_else(|_| self.get_collection_by_alias("session"))
253 .or_else(|_| async {
254 let mut collections = self.get_all_collections().await?;
255 if collections.is_empty() {
256 Err(Error::NoResult)
257 } else {
258 Ok(collections.swap_remove(0))
259 }
260 })
261 .await
262 }
263
264 /// Creates a new collection with a label and an alias.
265 pub async fn create_collection(
266 &self,
267 label: &str,
268 alias: &str,
269 ) -> Result<Collection<'_>, Error> {
270 let mut properties: HashMap<&str, Value> = HashMap::new();
271 properties.insert(SS_COLLECTION_LABEL, label.into());
272
273 let created_collection = self
274 .service_proxy
275 .create_collection(properties, alias)
276 .await?;
277
278 // This prompt handling is practically identical to create_collection
279 let collection_path: ObjectPath = {
280 // Get path of created object
281 let created_path = created_collection.collection;
282
283 // Check if that path is "/", if so should execute a prompt
284 if created_path.as_str() == "/" {
285 let prompt_path = created_collection.prompt;
286
287 // Exec prompt and parse result
288 let prompt_res = exec_prompt(self.conn.clone(), &prompt_path).await?;
289 prompt_res.try_into()?
290 } else {
291 // if not, just return created path
292 created_path.into()
293 }
294 };
295
296 Collection::new(
297 self.conn.clone(),
298 &self.session,
299 &self.service_proxy,
300 collection_path.into(),
301 )
302 .await
303 }
304
305 /// Searches all items by attributes
306 pub async fn search_items(
307 &self,
308 attributes: HashMap<&str, &str>,
309 ) -> Result<SearchItemsResult<Item<'_>>, Error> {
310 let items = self.service_proxy.search_items(attributes).await?;
311
312 let object_paths_to_items = |items: Vec<_>| {
313 futures_util::future::join_all(items.into_iter().map(|item_path| {
314 Item::new(
315 self.conn.clone(),
316 &self.session,
317 &self.service_proxy,
318 item_path,
319 )
320 }))
321 };
322
323 Ok(SearchItemsResult {
324 unlocked: object_paths_to_items(items.unlocked)
325 .await
326 .into_iter()
327 .collect::<Result<_, _>>()?,
328 locked: object_paths_to_items(items.locked)
329 .await
330 .into_iter()
331 .collect::<Result<_, _>>()?,
332 })
333 }
334
335 /// Unlock all items in a batch
336 pub async fn unlock_all(&self, items: &[&Item<'_>]) -> Result<(), Error> {
337 let objects = items.iter().map(|i| &*i.item_path).collect();
338 let lock_action_res = self.service_proxy.unlock(objects).await?;
339
340 if lock_action_res.object_paths.is_empty() {
341 exec_prompt(self.conn.clone(), &lock_action_res.prompt).await?;
342 }
343
344 Ok(())
345 }
346
347 pub async fn get_item_by_path(&'a self, item_path: OwnedObjectPath) -> Result<Item<'a>, Error> {
348 Item::new(
349 self.conn.clone(),
350 &self.session,
351 &self.service_proxy,
352 item_path,
353 )
354 .await
355 }
356
357 pub async fn get_collection_by_path(
358 &'a self,
359 collection_path: OwnedObjectPath,
360 ) -> Result<Collection<'a>, Error> {
361 Collection::new(
362 self.conn.clone(),
363 &self.session,
364 &self.service_proxy,
365 collection_path,
366 )
367 .await
368 }
369}
370
371#[cfg(test)]
372mod test {
373 use super::*;
374 use std::convert::TryFrom;
375 use zbus::zvariant::ObjectPath;
376
377 #[tokio::test]
378 async fn should_create_secret_service() {
379 SecretService::connect(EncryptionType::Plain).await.unwrap();
380 }
381
382 #[tokio::test]
383 async fn should_get_all_collections() {
384 // Assumes that there will always be a default collection
385 let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
386 let collections = ss.get_all_collections().await.unwrap();
387 assert!(!collections.is_empty(), "no collections found");
388 }
389
390 #[tokio::test]
391 async fn should_get_collection_by_alias() {
392 let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
393 ss.get_collection_by_alias("session").await.unwrap();
394 }
395
396 #[tokio::test]
397 async fn should_return_error_if_collection_doesnt_exist() {
398 let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
399
400 match ss
401 .get_collection_by_alias("definitely_defintely_does_not_exist")
402 .await
403 {
404 Err(Error::NoResult) => {}
405 _ => panic!(),
406 };
407 }
408
409 #[tokio::test]
410 async fn should_get_default_collection() {
411 let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
412 ss.get_default_collection().await.unwrap();
413 }
414
415 #[tokio::test]
416 async fn should_get_any_collection() {
417 let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
418 let _ = ss.get_any_collection().await.unwrap();
419 }
420
421 #[test_with::no_env(GITHUB_ACTIONS)]
422 #[tokio::test]
423 async fn should_create_and_delete_collection() {
424 let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
425 let test_collection = ss.create_collection("Test", "").await.unwrap();
426 assert_eq!(
427 ObjectPath::from(test_collection.collection_path.clone()),
428 ObjectPath::try_from("/org/freedesktop/secrets/collection/Test").unwrap()
429 );
430 test_collection.delete().await.unwrap();
431 }
432
433 #[tokio::test]
434 async fn should_search_items() {
435 let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
436 let collection = ss.get_default_collection().await.unwrap();
437
438 // Create an item
439 let item = collection
440 .create_item(
441 "test",
442 HashMap::from([("test_attribute_in_ss", "test_value")]),
443 b"test_secret",
444 false,
445 "text/plain",
446 )
447 .await
448 .unwrap();
449
450 // handle empty vec search
451 ss.search_items(HashMap::new()).await.unwrap();
452
453 // handle no result
454 let bad_search = ss
455 .search_items(HashMap::from([("test", "test")]))
456 .await
457 .unwrap();
458 assert_eq!(bad_search.unlocked.len(), 0);
459 assert_eq!(bad_search.locked.len(), 0);
460
461 // handle correct search for item and compare
462 let search_item = ss
463 .search_items(HashMap::from([("test_attribute_in_ss", "test_value")]))
464 .await
465 .unwrap();
466
467 assert_eq!(item.item_path, search_item.unlocked[0].item_path);
468 assert_eq!(search_item.locked.len(), 0);
469 item.delete().await.unwrap();
470 }
471}