[go: up one dir, main page]

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}