diff --git a/Cargo.toml b/Cargo.toml index 26d542a..c896707 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ keywords = ["password", "credential", "keychain", "keyring", "cross-platform"] license = "MIT OR Apache-2.0" name = "keyring" repository = "https://github.com/hwchen/keyring-rs.git" -version = "2.0.5" +version = "2.1.0" edition = "2021" exclude = [".github/"] readme = "README.md" @@ -48,7 +48,7 @@ crate-type = ["staticlib"] [dev-dependencies] clap = { version = "4", features = ["derive", "wrap_help"] } -rpassword = "7.0" +rpassword = "7" rand = "0.8" doc-comment = "0.3" -whoami = "1.2" +whoami = "1" diff --git a/README.md b/README.md index d4f463c..6249ebf 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ Thanks to the following for helping make this library better, whether through contributing code, discussion, or bug reports! - @Alexei-Barnes +- @benwr - @bhkaminski - @brotskydotcom - @complexspaces diff --git a/src/credential.rs b/src/credential.rs index 1474914..632ddc5 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -27,7 +27,7 @@ pub trait CredentialApi { /// so a second call to delete_password will return /// a [NoEntry](crate::Error::NoEntry) error. fn delete_password(&self) -> Result<()>; - /// Return the underlying concrete object cast to [Any](std::any::Any). + /// Return the underlying concrete object cast to [Any]. /// /// This allows clients /// to downcast the credential to its concrete type so they @@ -45,6 +45,20 @@ impl std::fmt::Debug for Credential { /// A thread-safe implementation of the [Credential API](CredentialApi). pub type Credential = dyn CredentialApi + Send + Sync; +/// A descriptor for the lifetime of stored credentials, returned from +/// a credential store's [persistence](CredentialBuilderApi::persistence) call. +#[non_exhaustive] +pub enum CredentialPersistence { + /// Credentials vanish when the entry vanishes (stored in the entry) + EntryOnly, + /// Credentials vanish when the process terminates (stored in process memory) + ProcessOnly, + /// Credentials persist until the machine reboots (stored in kernel memory) + UntilReboot, + /// Credentials persist until they are explicitly deleted (stored on disk) + UntilDelete, +} + /// The API that [credential builders](CredentialBuilder) implement. pub trait CredentialBuilderApi { /// Create a credential identified by the given target, service, and user. @@ -52,12 +66,21 @@ pub trait CredentialBuilderApi { /// This typically has no effect on the content of the underlying store. /// A credential need not be persisted until its password is set. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result>; - /// Return the underlying concrete object cast to [Any](std::any::Any). + /// Return the underlying concrete object cast to [Any]. /// /// Because credential builders need not have any internal structure, /// this call is not so much for clients /// as it is to allow automatic derivation of a Debug trait for builders. fn as_any(&self) -> &dyn Any; + + /// The lifetime of credentials produced by this builder. + /// + /// A default implementation is provided for backward compatibility, + /// since this API was added in a minor release. The default assumes + /// that keystores use disk-based credential storage. + fn persistence(&self) -> CredentialPersistence { + CredentialPersistence::UntilDelete + } } impl std::fmt::Debug for CredentialBuilder { diff --git a/src/keyutils.rs b/src/keyutils.rs index 8bea6b8..21681bd 100644 --- a/src/keyutils.rs +++ b/src/keyutils.rs @@ -97,7 +97,9 @@ Alternatively, you can drop the secret-service credential store altogether with `--no-default-features` and `--features linux-no-secret-service`. */ -use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; +use super::credential::{ + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, +}; use super::error::{decode_password, Error as ErrorCode, Result}; use linux_keyutils::{KeyError, KeyRing, KeyRingIdentifier}; @@ -284,6 +286,12 @@ impl CredentialBuilderApi for KeyutilsCredentialBuilder { fn as_any(&self) -> &dyn std::any::Any { self } + + /// Since this keystore keeps credentials in kernel memory, + /// they vanish on reboot + fn persistence(&self) -> CredentialPersistence { + CredentialPersistence::UntilReboot + } } /// Map an underlying keyutils error to a platform-independent error with annotation. @@ -313,9 +321,18 @@ fn wrap(err: KeyError) -> Box { #[cfg(test)] mod tests { + use crate::credential::CredentialPersistence; use crate::{tests::generate_random_string, Entry, Error}; - use super::KeyutilsCredential; + use super::{default_credential_builder, KeyutilsCredential}; + + #[test] + fn test_persistence() { + assert!(matches!( + default_credential_builder().persistence(), + CredentialPersistence::UntilReboot + )) + } fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor(KeyutilsCredential::new_with_target, service, user) diff --git a/src/macos.rs b/src/macos.rs index 968d0a6..42bb56c 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -238,9 +238,18 @@ pub fn decode_error(err: Error) -> ErrorCode { #[cfg(test)] mod tests { + use crate::credential::CredentialPersistence; use crate::{tests::generate_random_string, Entry, Error}; - use super::MacCredential; + use super::{default_credential_builder, MacCredential}; + + #[test] + fn test_persistence() { + assert!(matches!( + default_credential_builder().persistence(), + CredentialPersistence::UntilDelete + )) + } fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor(MacCredential::new_with_target, service, user) diff --git a/src/mock.rs b/src/mock.rs index fb7bebf..e8e2e3e 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -14,8 +14,8 @@ 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 peristence between -runs, so getting a credential before setting it will always result +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 @@ -36,7 +36,9 @@ entry.set_password("test").expect("error has been cleared"); use std::cell::RefCell; use std::sync::Mutex; -use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; +use super::credential::{ + Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, +}; use super::error::{Error, Result}; /// The concrete mock credential @@ -180,6 +182,11 @@ impl CredentialBuilderApi for MockCredentialBuilder { 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. @@ -189,9 +196,18 @@ pub fn default_credential_builder() -> Box { #[cfg(test)] mod tests { - use super::MockCredential; + 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)) diff --git a/src/secret_service.rs b/src/secret_service.rs index a8a427b..a9f4484 100644 --- a/src/secret_service.rs +++ b/src/secret_service.rs @@ -440,9 +440,18 @@ fn wrap(err: Error) -> Box { #[cfg(test)] mod tests { + use crate::credential::CredentialPersistence; use crate::{tests::generate_random_string, Entry, Error}; - use super::SsCredential; + use super::{default_credential_builder, SsCredential}; + + #[test] + fn test_persistence() { + assert!(matches!( + default_credential_builder().persistence(), + CredentialPersistence::UntilDelete + )) + } fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor(SsCredential::new_with_target, service, user) diff --git a/src/windows.rs b/src/windows.rs index dda96b8..91c0505 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -395,9 +395,18 @@ fn wrap(code: DWORD) -> Box { mod tests { use super::*; + use crate::credential::CredentialPersistence; use crate::tests::{generate_random_string, generate_random_string_of_len}; use crate::Entry; + #[test] + fn test_persistence() { + assert!(matches!( + default_credential_builder().persistence(), + CredentialPersistence::UntilDelete + )) + } + fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor(WinCredential::new_with_target, service, user) }