keyring/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2/*!
3
4# Keyring
5
6This is a cross-platform library that does storage and retrieval of passwords
7(or other secrets) in an underlying platform-specific secure store.
8A top-level introduction to the library's usage, as well as a small code sample,
9may be found in [the library's entry on crates.io](https://crates.io/crates/keyring).
10Currently supported platforms are
11Linux,
12FreeBSD,
13OpenBSD,
14Windows,
15macOS, and iOS.
16
17## Design
18
19This crate implements a very simple, platform-independent concrete object called an _entry_.
20Each entry is identified by a <_service name_, _user name_> pair of UTF-8 strings,
21optionally augmented by a _target_ string (which can be used to distinguish two entries
22that have the same _service name_ and _user name_).
23Entries support setting, getting, and forgetting (aka deleting) passwords (UTF-8 strings)
24and binary secrets (byte arrays).
25
26Entries provide persistence for their passwords by wrapping credentials held in platform-specific
27credential stores. The implementations of these platform-specific stores are captured
28in two types (with associated traits):
29
30- a _credential builder_, represented by the [CredentialBuilder] type
31 (and [CredentialBuilderApi](credential::CredentialBuilderApi) trait). Credential
32 builders are given the identifying information provided for an entry and map
33 it to the identifying information for a platform-specific credential.
34- a _credential_, represented by the [Credential] type
35 (and [CredentialApi](credential::CredentialApi) trait). The platform-specific credential
36 identified by a builder for an entry is what provides the secure storage
37 for that entry's password/secret.
38
39## Crate-provided Credential Stores
40
41This crate runs on several different platforms, and it provides one
42or more implementations of credential stores on each platform.
43These implementations work by mapping the data used to identify an entry
44to data used to identify platform-specific storage objects.
45For example, on macOS, the service and user provided for an entry
46are mapped to the service and user attributes that identify a
47generic credential in the macOS keychain.
48
49Typically, platform-specific stores (called _keystores_ in this crate)
50have a richer model of a credential than
51the one used by this crate to identify entries.
52These keystores expose their specific model in the
53concrete credential objects they use to implement the Credential trait.
54In order to allow clients to access this richer model, the Credential trait
55has an [as_any](credential::CredentialApi::as_any) method that returns a
56reference to the underlying
57concrete object typed as [Any](std::any::Any), so that it can be downgraded to
58its concrete type.
59
60### Credential store features
61
62Each of the platform-specific credential stores is associated with one or more features.
63These features control whether that store is included when the crate is built. For
64example, the macOS Keychain credential store is only included if the `"apple-native"`
65feature is specified (and the crate is built with a macOS target).
66
67If no specified credential store features apply to a given platform,
68this crate will use the (platform-independent) _mock_ credential store (see below)
69on that platform. There are no
70default features in this crate: you must specify explicitly which platform-specific
71credential stores you intend to use.
72
73Here are the available credential store features:
74
75- `apple-native`: Provides access to the Keychain credential store on macOS and iOS.
76
77- `windows-native`: Provides access to the Windows Credential Store on Windows.
78
79- `linux-native`: Provides access to the `keyutils` storage on Linux.
80
81- `linux-native-sync-persistent`: Uses both `keyutils` and `sync-secret-service`
82 (see below) for storage. See the docs for the `keyutils_persistent`
83 module for a full explanation of why both are used. Because this
84 store uses the `sync-secret-service`, you can use additional features related
85 to that store (described below).
86
87- `linux-native-async-persistent`: Uses both `keyutils` and `async-secret-service`
88 (see below) for storage. See the docs for the `keyutils_persistent`
89 module for a full explanation of why both are used.
90 Because this store uses the `async-secret-service`, you
91 must specify the additional features required by that store (described below).
92
93- `sync-secret-service`: Provides access to the DBus-based
94 [Secret Service](https://specifications.freedesktop.org/secret-service/latest/)
95 storage on Linux, FreeBSD, and OpenBSD. This is a _synchronous_ keystore that provides
96 support for encrypting secrets when they are transferred across the bus. If you wish
97 to use this encryption support, additionally specify one (and only one) of the
98 `crypto-rust` or `crypto-openssl` features (to choose the implementation libraries
99 used for the encryption). By default, this keystore requires that the DBus library be
100 installed on the user's machine (and the openSSL library if you specify it for
101 encryption), but you can avoid this requirement by specifying the `vendored` feature
102 (which will cause the build to include those libraries statically).
103
104- `async-secret-service`: Provides access to the DBus-based
105 [Secret Service](https://specifications.freedesktop.org/secret-service/latest/)
106 storage on Linux, FreeBSD, and OpenBSD. This is an _asynchronous_ keystore that
107 always encrypts secrets when they are transferred across the bus. You _must_ specify
108 both an async runtime feature (either `tokio` or `async-io`) and a cryptographic
109 implementation (either `crypto-rust` or `crypto-openssl`) when using this
110 keystore. If you want to use openSSL encryption but those libraries are not
111 installed on the user's machine, specify the `vendored` feature
112 to statically link them with the built crate.
113
114The Linux platform is the only one for which this crate supplies multiple keystores:
115native (keyutils), sync or async secret service, and sync or async "combo" (both
116keyutils and secret service). You cannot specify use of both sync and async
117keystores; this will lead to a compile error. If you enable a combo keystore on Linux,
118that will be the default keystore. If you don't enable a
119combo keystore on Linux, but you do enable both the native and secret service keystores,
120the secret service will be the default.
121
122## Client-provided Credential Stores
123
124In addition to the platform stores implemented by this crate, clients
125are free to provide their own secure stores and use those. There are
126two mechanisms provided for this:
127
128- Clients can give their desired credential builder to the crate
129 for use by the [Entry::new] and [Entry::new_with_target] calls.
130 This is done by making a call to [set_default_credential_builder].
131 The major advantage of this approach is that client code remains
132 independent of the credential builder being used.
133
134- Clients can construct their concrete credentials directly and
135 then turn them into entries by using the [Entry::new_with_credential]
136 call. The major advantage of this approach is that credentials
137 can be identified however clients want, rather than being restricted
138 to the simple model used by this crate.
139
140## Mock Credential Store
141
142In addition to the platform-specific credential stores, this crate
143always provides a mock credential store that clients can use to
144test their code in a platform independent way. The mock credential
145store allows for pre-setting errors as well as password values to
146be returned from [Entry] method calls.
147
148## Interoperability with Third Parties
149
150Each of the platform-specific credential stores provided by this crate uses
151an underlying store that may also be used by modules written
152in other languages. If you want to interoperate with these third party
153credential writers, then you will need to understand the details of how the
154target, service, and user of this crate's generic model
155are used to identify credentials in the platform-specific store.
156These details are in the implementation of this crate's secure-storage
157modules, and are documented in the headers of those modules.
158
159(_N.B._ Since the included credential store implementations are platform-specific,
160you may need to use the Platform drop-down on [docs.rs](https://docs.rs/keyring) to
161view the storage module documentation for your desired platform.)
162
163## Caveats
164
165This module expects passwords to be UTF-8 encoded strings,
166so if a third party has stored an arbitrary byte string
167then retrieving that as a password will return a
168[BadEncoding](Error::BadEncoding) error.
169The returned error will have the raw bytes attached,
170so you can access them, but you can also just fetch
171them directly using [get_secret](Entry::get_secret) rather than
172[get_password](Entry::get_password).
173
174While this crate's code is thread-safe, the underlying credential
175stores may not handle access from different threads reliably.
176In particular, accessing the same credential
177from multiple threads at the same time can fail, especially on
178Windows and Linux, because the accesses may not be serialized in the same order
179they are made. And for RPC-based credential stores such as the dbus-based Secret
180Service, accesses from multiple threads (and even the same thread very quickly)
181are not recommended, as they may cause the RPC mechanism to fail.
182 */
183
184use log::debug;
185use std::collections::HashMap;
186
187pub use credential::{Credential, CredentialBuilder};
188pub use error::{Error, Result};
189
190pub mod mock;
191
192//
193// can't use both sync and async secret service
194//
195#[cfg(any(
196 all(feature = "sync-secret-service", feature = "async-secret-service"),
197 all(
198 feature = "linux-native-sync-persistent",
199 feature = "linux-native-async-persistent",
200 )
201))]
202compile_error!("This crate cannot use both the sync and async versions of any credential store");
203
204//
205// pick the *nix keystore
206//
207#[cfg(all(target_os = "linux", feature = "linux-native"))]
208#[cfg_attr(docsrs, doc(cfg(target_os = "linux")))]
209pub mod keyutils;
210#[cfg(all(
211 target_os = "linux",
212 feature = "linux-native",
213 not(feature = "sync-secret-service"),
214 not(feature = "async-secret-service"),
215))]
216pub use keyutils as default;
217
218#[cfg(all(
219 any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
220 any(feature = "sync-secret-service", feature = "async-secret-service"),
221))]
222#[cfg_attr(
223 docsrs,
224 doc(cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd")))
225)]
226pub mod secret_service;
227#[cfg(all(
228 any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"),
229 any(feature = "sync-secret-service", feature = "async-secret-service"),
230 not(any(
231 feature = "linux-native-sync-persistent",
232 feature = "linux-native-async-persistent",
233 )),
234))]
235pub use secret_service as default;
236
237#[cfg(all(
238 target_os = "linux",
239 any(
240 feature = "linux-native-sync-persistent",
241 feature = "linux-native-async-persistent",
242 )
243))]
244#[cfg_attr(docsrs, doc(cfg(target_os = "linux")))]
245pub mod keyutils_persistent;
246#[cfg(all(
247 target_os = "linux",
248 any(
249 feature = "linux-native-sync-persistent",
250 feature = "linux-native-async-persistent",
251 ),
252))]
253pub use keyutils_persistent as default;
254
255// fallback to mock if neither keyutils nor secret service is available
256#[cfg(any(
257 all(
258 target_os = "linux",
259 not(feature = "linux-native"),
260 not(feature = "sync-secret-service"),
261 not(feature = "async-secret-service"),
262 ),
263 all(
264 any(target_os = "freebsd", target_os = "openbsd"),
265 not(feature = "sync-secret-service"),
266 not(feature = "async-secret-service"),
267 )
268))]
269pub use mock as default;
270
271//
272// pick the Apple keystore
273//
274#[cfg(all(target_os = "macos", feature = "apple-native"))]
275#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
276pub mod macos;
277#[cfg(all(target_os = "macos", feature = "apple-native"))]
278pub use macos as default;
279#[cfg(all(target_os = "macos", not(feature = "apple-native")))]
280pub use mock as default;
281
282#[cfg(all(target_os = "ios", feature = "apple-native"))]
283#[cfg_attr(docsrs, doc(cfg(target_os = "ios")))]
284pub mod ios;
285#[cfg(all(target_os = "ios", feature = "apple-native"))]
286pub use ios as default;
287#[cfg(all(target_os = "ios", not(feature = "apple-native")))]
288pub use mock as default;
289
290//
291// pick the Windows keystore
292//
293#[cfg(all(target_os = "windows", feature = "windows-native"))]
294#[cfg_attr(docsrs, doc(cfg(target_os = "windows")))]
295pub mod windows;
296#[cfg(all(target_os = "windows", not(feature = "windows-native")))]
297pub use mock as default;
298#[cfg(all(target_os = "windows", feature = "windows-native"))]
299pub use windows as default;
300
301#[cfg(not(any(
302 target_os = "linux",
303 target_os = "freebsd",
304 target_os = "openbsd",
305 target_os = "macos",
306 target_os = "ios",
307 target_os = "windows",
308)))]
309pub use mock as default;
310
311pub mod credential;
312pub mod error;
313
314#[derive(Default, Debug)]
315struct EntryBuilder {
316 inner: Option<Box<CredentialBuilder>>,
317}
318
319static DEFAULT_BUILDER: std::sync::RwLock<EntryBuilder> =
320 std::sync::RwLock::new(EntryBuilder { inner: None });
321
322/// Set the credential builder used by default to create entries.
323///
324/// This is really meant for use by clients who bring their own credential
325/// store and want to use it everywhere. If you are using multiple credential
326/// stores and want precise control over which credential is in which store,
327/// then use [new_with_credential](Entry::new_with_credential).
328///
329/// This will block waiting for all other threads currently creating entries
330/// to complete what they are doing. It's really meant to be called
331/// at app startup before you start creating entries.
332pub fn set_default_credential_builder(new: Box<CredentialBuilder>) {
333 let mut guard = DEFAULT_BUILDER
334 .write()
335 .expect("Poisoned RwLock in keyring-rs: please report a bug!");
336 guard.inner = Some(new);
337}
338
339fn build_default_credential(target: Option<&str>, service: &str, user: &str) -> Result<Entry> {
340 static DEFAULT: std::sync::OnceLock<Box<CredentialBuilder>> = std::sync::OnceLock::new();
341 let guard = DEFAULT_BUILDER
342 .read()
343 .expect("Poisoned RwLock in keyring-rs: please report a bug!");
344 let builder = guard
345 .inner
346 .as_ref()
347 .unwrap_or_else(|| DEFAULT.get_or_init(|| default::default_credential_builder()));
348 let credential = builder.build(target, service, user)?;
349 Ok(Entry { inner: credential })
350}
351
352#[derive(Debug)]
353pub struct Entry {
354 inner: Box<Credential>,
355}
356
357impl Entry {
358 /// Create an entry for the given service and user.
359 ///
360 /// The default credential builder is used.
361 ///
362 /// # Errors
363 ///
364 /// This function will return an [Error] if the `service` or `user` values are invalid.
365 /// The specific reasons for invalidity are platform-dependent, but include length constraints.
366 ///
367 /// # Panics
368 ///
369 /// In the very unlikely event that the internal credential builder's `RwLock`` is poisoned, this function
370 /// will panic. If you encounter this, and especially if you can reproduce it, please report a bug with the
371 /// details (and preferably a backtrace) so the developers can investigate.
372 pub fn new(service: &str, user: &str) -> Result<Entry> {
373 debug!("creating entry with service {service}, user {user}, and no target");
374 let entry = build_default_credential(None, service, user)?;
375 debug!("created entry {:?}", entry.inner);
376 Ok(entry)
377 }
378
379 /// Create an entry for the given target, service, and user.
380 ///
381 /// The default credential builder is used.
382 pub fn new_with_target(target: &str, service: &str, user: &str) -> Result<Entry> {
383 debug!("creating entry with service {service}, user {user}, and target {target}");
384 let entry = build_default_credential(Some(target), service, user)?;
385 debug!("created entry {:?}", entry.inner);
386 Ok(entry)
387 }
388
389 /// Create an entry that uses the given platform credential for storage.
390 pub fn new_with_credential(credential: Box<Credential>) -> Entry {
391 debug!("create entry from {credential:?}");
392 Entry { inner: credential }
393 }
394
395 /// Set the password for this entry.
396 ///
397 /// Can return an [Ambiguous](Error::Ambiguous) error
398 /// if there is more than one platform credential
399 /// that matches this entry. This can only happen
400 /// on some platforms, and then only if a third-party
401 /// application wrote the ambiguous credential.
402 pub fn set_password(&self, password: &str) -> Result<()> {
403 debug!("set password for entry {:?}", self.inner);
404 self.inner.set_password(password)
405 }
406
407 /// Set the secret for this entry.
408 ///
409 /// Can return an [Ambiguous](Error::Ambiguous) error
410 /// if there is more than one platform credential
411 /// that matches this entry. This can only happen
412 /// on some platforms, and then only if a third-party
413 /// application wrote the ambiguous credential.
414 pub fn set_secret(&self, secret: &[u8]) -> Result<()> {
415 debug!("set secret for entry {:?}", self.inner);
416 self.inner.set_secret(secret)
417 }
418
419 /// Retrieve the password saved for this entry.
420 ///
421 /// Returns a [NoEntry](Error::NoEntry) error if there isn't one.
422 ///
423 /// Can return an [Ambiguous](Error::Ambiguous) error
424 /// if there is more than one platform credential
425 /// that matches this entry. This can only happen
426 /// on some platforms, and then only if a third-party
427 /// application wrote the ambiguous credential.
428 pub fn get_password(&self) -> Result<String> {
429 debug!("get password from entry {:?}", self.inner);
430 self.inner.get_password()
431 }
432
433 /// Retrieve the secret saved for this entry.
434 ///
435 /// Returns a [NoEntry](Error::NoEntry) error if there isn't one.
436 ///
437 /// Can return an [Ambiguous](Error::Ambiguous) error
438 /// if there is more than one platform credential
439 /// that matches this entry. This can only happen
440 /// on some platforms, and then only if a third-party
441 /// application wrote the ambiguous credential.
442 pub fn get_secret(&self) -> Result<Vec<u8>> {
443 debug!("get secret from entry {:?}", self.inner);
444 self.inner.get_secret()
445 }
446
447 /// Get the attributes on the underlying credential for this entry.
448 ///
449 /// Some of the underlying credential stores allow credentials to have named attributes
450 /// that can be set to string values. See the documentation for each credential store
451 /// for a list of which attribute names are supported by that store.
452 ///
453 /// Returns a [NoEntry](Error::NoEntry) error if there isn't a credential for this entry.
454 ///
455 /// Can return an [Ambiguous](Error::Ambiguous) error
456 /// if there is more than one platform credential
457 /// that matches this entry. This can only happen
458 /// on some platforms, and then only if a third-party
459 /// application wrote the ambiguous credential.
460 pub fn get_attributes(&self) -> Result<HashMap<String, String>> {
461 debug!("get attributes from entry {:?}", self.inner);
462 self.inner.get_attributes()
463 }
464
465 /// Update the attributes on the underlying credential for this entry.
466 ///
467 /// Some of the underlying credential stores allow credentials to have named attributes
468 /// that can be set to string values. See the documentation for each credential store
469 /// for a list of which attribute names can be given values by this call. To support
470 /// cross-platform use, each credential store ignores (without error) any specified attributes
471 /// that aren't supported by that store.
472 ///
473 /// Returns a [NoEntry](Error::NoEntry) error if there isn't a credential for this entry.
474 ///
475 /// Can return an [Ambiguous](Error::Ambiguous) error
476 /// if there is more than one platform credential
477 /// that matches this entry. This can only happen
478 /// on some platforms, and then only if a third-party
479 /// application wrote the ambiguous credential.
480 pub fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
481 debug!(
482 "update attributes for entry {:?} from map {attributes:?}",
483 self.inner
484 );
485 self.inner.update_attributes(attributes)
486 }
487
488 /// Delete the underlying credential for this entry.
489 ///
490 /// Returns a [NoEntry](Error::NoEntry) error if there isn't one.
491 ///
492 /// Can return an [Ambiguous](Error::Ambiguous) error
493 /// if there is more than one platform credential
494 /// that matches this entry. This can only happen
495 /// on some platforms, and then only if a third-party
496 /// application wrote the ambiguous credential.
497 ///
498 /// Note: This does _not_ affect the lifetime of the [Entry]
499 /// structure, which is controlled by Rust. It only
500 /// affects the underlying credential store.
501 pub fn delete_credential(&self) -> Result<()> {
502 debug!("delete entry {:?}", self.inner);
503 self.inner.delete_credential()
504 }
505
506 /// Return a reference to this entry's wrapped credential.
507 ///
508 /// The reference is of the [Any](std::any::Any) type, so it can be
509 /// downgraded to a concrete credential object. The client must know
510 /// what type of concrete object to cast to.
511 pub fn get_credential(&self) -> &dyn std::any::Any {
512 self.inner.as_any()
513 }
514}
515
516#[cfg(doctest)]
517doc_comment::doctest!("../README.md", readme);
518
519#[cfg(test)]
520/// There are no actual tests in this module.
521/// Instead, it contains generics that each keystore invokes in their tests,
522/// passing their store-specific parameters for the generic ones.
523//
524// Since iOS doesn't use any of these generics, we allow dead code.
525#[allow(dead_code)]
526mod tests {
527 use super::{credential::CredentialApi, Entry, Error, Result};
528 use std::collections::HashMap;
529
530 /// Create a platform-specific credential given the constructor, service, and user
531 pub fn entry_from_constructor<F, T>(f: F, service: &str, user: &str) -> Entry
532 where
533 F: FnOnce(Option<&str>, &str, &str) -> Result<T>,
534 T: 'static + CredentialApi + Send + Sync,
535 {
536 match f(None, service, user) {
537 Ok(credential) => Entry::new_with_credential(Box::new(credential)),
538 Err(err) => {
539 panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
540 }
541 }
542 }
543
544 /// Create a platform-specific credential given the constructor, service, user, and attributes
545 pub fn entry_from_constructor_and_attributes<F, T>(
546 f: F,
547 service: &str,
548 user: &str,
549 attrs: &HashMap<&str, &str>,
550 ) -> Entry
551 where
552 F: FnOnce(Option<&str>, &str, &str, &HashMap<&str, &str>) -> Result<T>,
553 T: 'static + CredentialApi + Send + Sync,
554 {
555 match f(None, service, user, attrs) {
556 Ok(credential) => Entry::new_with_credential(Box::new(credential)),
557 Err(err) => {
558 panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
559 }
560 }
561 }
562
563 /// A basic round-trip unit test given an entry and a password.
564 pub fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
565 entry
566 .set_password(in_pass)
567 .unwrap_or_else(|err| panic!("Can't set password for {case}: {err:?}"));
568 let out_pass = entry
569 .get_password()
570 .unwrap_or_else(|err| panic!("Can't get password for {case}: {err:?}"));
571 assert_eq!(
572 in_pass, out_pass,
573 "Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
574 );
575 entry
576 .delete_credential()
577 .unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
578 let password = entry.get_password();
579 assert!(
580 matches!(password, Err(Error::NoEntry)),
581 "Read deleted password for {case}",
582 );
583 }
584
585 /// A basic round-trip unit test given an entry and a password.
586 pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
587 entry
588 .set_secret(in_secret)
589 .unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
590 let out_secret = entry
591 .get_secret()
592 .unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
593 assert_eq!(
594 in_secret, &out_secret,
595 "Passwords don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'",
596 );
597 entry
598 .delete_credential()
599 .unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
600 let password = entry.get_secret();
601 assert!(
602 matches!(password, Err(Error::NoEntry)),
603 "Read deleted password for {case}",
604 );
605 }
606
607 /// When tests fail, they leave keys behind, and those keys
608 /// have to be cleaned up before the tests can be run again
609 /// in order to avoid bad results. So it's a lot easier just
610 /// to have tests use a random string for key names to avoid
611 /// the conflicts, and then do any needed cleanup once everything
612 /// is working correctly. So we export this function for tests to use.
613 pub fn generate_random_string_of_len(len: usize) -> String {
614 use fastrand;
615 use std::iter::repeat_with;
616 repeat_with(fastrand::alphanumeric).take(len).collect()
617 }
618
619 pub fn generate_random_string() -> String {
620 generate_random_string_of_len(30)
621 }
622
623 fn generate_random_bytes_of_len(len: usize) -> Vec<u8> {
624 use fastrand;
625 use std::iter::repeat_with;
626 repeat_with(|| fastrand::u8(..)).take(len).collect()
627 }
628
629 pub fn test_empty_service_and_user<F>(f: F)
630 where
631 F: Fn(&str, &str) -> Entry,
632 {
633 let name = generate_random_string();
634 let in_pass = "doesn't matter";
635 test_round_trip("empty user", &f(&name, ""), in_pass);
636 test_round_trip("empty service", &f("", &name), in_pass);
637 test_round_trip("empty service & user", &f("", ""), in_pass);
638 }
639
640 pub fn test_missing_entry<F>(f: F)
641 where
642 F: FnOnce(&str, &str) -> Entry,
643 {
644 let name = generate_random_string();
645 let entry = f(&name, &name);
646 assert!(
647 matches!(entry.get_password(), Err(Error::NoEntry)),
648 "Missing entry has password"
649 )
650 }
651
652 pub fn test_empty_password<F>(f: F)
653 where
654 F: FnOnce(&str, &str) -> Entry,
655 {
656 let name = generate_random_string();
657 let entry = f(&name, &name);
658 test_round_trip("empty password", &entry, "");
659 }
660
661 pub fn test_round_trip_ascii_password<F>(f: F)
662 where
663 F: FnOnce(&str, &str) -> Entry,
664 {
665 let name = generate_random_string();
666 let entry = f(&name, &name);
667 test_round_trip("ascii password", &entry, "test ascii password");
668 }
669
670 pub fn test_round_trip_non_ascii_password<F>(f: F)
671 where
672 F: FnOnce(&str, &str) -> Entry,
673 {
674 let name = generate_random_string();
675 let entry = f(&name, &name);
676 test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
677 }
678
679 pub fn test_round_trip_random_secret<F>(f: F)
680 where
681 F: FnOnce(&str, &str) -> Entry,
682 {
683 let name = generate_random_string();
684 let entry = f(&name, &name);
685 let secret = generate_random_bytes_of_len(24);
686 test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
687 }
688
689 pub fn test_update<F>(f: F)
690 where
691 F: FnOnce(&str, &str) -> Entry,
692 {
693 let name = generate_random_string();
694 let entry = f(&name, &name);
695 test_round_trip("initial ascii password", &entry, "test ascii password");
696 test_round_trip(
697 "updated non-ascii password",
698 &entry,
699 "このきれいな花は桜です",
700 );
701 }
702
703 pub fn test_noop_get_update_attributes<F>(f: F)
704 where
705 F: FnOnce(&str, &str) -> Entry,
706 {
707 let name = generate_random_string();
708 let entry = f(&name, &name);
709 assert!(
710 matches!(entry.get_attributes(), Err(Error::NoEntry)),
711 "Read missing credential in attribute test",
712 );
713 let mut map: HashMap<&str, &str> = HashMap::new();
714 map.insert("test attribute name", "test attribute value");
715 assert!(
716 matches!(entry.update_attributes(&map), Err(Error::NoEntry)),
717 "Updated missing credential in attribute test",
718 );
719 // create the credential and test again
720 entry
721 .set_password("test password for attributes")
722 .unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
723 match entry.get_attributes() {
724 Err(err) => panic!("Couldn't get attributes: {err:?}"),
725 Ok(attrs) if attrs.is_empty() => {}
726 Ok(attrs) => panic!("Unexpected attributes: {attrs:?}"),
727 }
728 assert!(
729 matches!(entry.update_attributes(&map), Ok(())),
730 "Couldn't update attributes in attribute test",
731 );
732 match entry.get_attributes() {
733 Err(err) => panic!("Couldn't get attributes after update: {err:?}"),
734 Ok(attrs) if attrs.is_empty() => {}
735 Ok(attrs) => panic!("Unexpected attributes after update: {attrs:?}"),
736 }
737 entry
738 .delete_credential()
739 .unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
740 assert!(
741 matches!(entry.get_attributes(), Err(Error::NoEntry)),
742 "Read deleted credential in attribute test",
743 );
744 }
745}