Switch to secret-service crate and async functions

This commit is contained in:
John Mumm 2025-07-16 11:42:29 +02:00
parent 68b6a6f14b
commit bda4e7bb4f
No known key found for this signature in database
GPG Key ID: 73D2271AFDC26EA8
14 changed files with 1141 additions and 969 deletions

298
Cargo.lock generated
View File

@ -198,6 +198,18 @@ dependencies = [
"xattr",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
@ -228,6 +240,17 @@ dependencies = [
"zstd-safe",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.88"
@ -1020,35 +1043,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
[[package]]
name = "dbus"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
dependencies = [
"libc",
"libdbus-sys",
"winapi",
]
[[package]]
name = "dbus-secret-service"
version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42a16374481d92aed73ae45b1f120207d8e71d24fb89f357fadbd8f946fd84b"
dependencies = [
"aes",
"block-padding",
"cbc",
"dbus",
"futures-util",
"hkdf",
"num",
"once_cell",
"rand",
"sha2",
]
[[package]]
name = "deadpool"
version = "0.10.0"
@ -1187,6 +1181,33 @@ dependencies = [
"encoding_rs",
]
[[package]]
name = "endi"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_filter"
version = "0.1.3"
@ -2215,15 +2236,6 @@ version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libdbus-sys"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
dependencies = [
"pkg-config",
]
[[package]]
name = "libmimalloc-sys"
version = "0.1.43"
@ -2378,6 +2390,15 @@ dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "miette"
version = "7.6.0"
@ -2512,6 +2533,7 @@ dependencies = [
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]
@ -2661,6 +2683,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "os_str_bytes"
version = "6.6.1"
@ -2947,6 +2979,15 @@ dependencies = [
"indexmap",
]
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit 0.22.27",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@ -3729,6 +3770,25 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "secret-service"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dccff79e916a339eec808de579764e3459658c903960d5aa4f7959ee9f6d5f2b"
dependencies = [
"aes",
"cbc",
"futures-util",
"generic-array",
"getrandom 0.2.16",
"hkdf",
"num",
"once_cell",
"serde",
"sha2",
"zbus",
]
[[package]]
name = "security-framework"
version = "3.2.0"
@ -3823,6 +3883,17 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "1.0.0"
@ -3995,6 +4066,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "statrs"
version = "0.18.0"
@ -4401,6 +4478,7 @@ dependencies = [
"slab",
"socket2 0.6.0",
"tokio-macros",
"tracing",
"windows-sys 0.59.0",
]
@ -4461,12 +4539,18 @@ dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.0"
@ -4476,6 +4560,17 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"toml_datetime 0.6.11",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.23.2"
@ -4485,7 +4580,7 @@ dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
"winnow",
@ -4706,6 +4801,17 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "uds_windows"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
"memoffset",
"tempfile",
"winapi",
]
[[package]]
name = "unicase"
version = "2.8.1"
@ -4925,7 +5031,7 @@ dependencies = [
"thiserror 2.0.12",
"tokio",
"toml",
"toml_edit",
"toml_edit 0.23.2",
"tracing",
"tracing-durations-export",
"tracing-subscriber",
@ -5109,7 +5215,7 @@ dependencies = [
"tempfile",
"thiserror 2.0.12",
"tokio",
"toml_edit",
"toml_edit 0.23.2",
"tracing",
"uv-cache-key",
"uv-configuration",
@ -5683,17 +5789,19 @@ dependencies = [
name = "uv-keyring"
version = "0.0.1"
dependencies = [
"async-trait",
"base64 0.22.1",
"byteorder",
"clap",
"dbus-secret-service",
"doc-comment",
"env_logger",
"fastrand",
"log",
"rpassword",
"rprompt",
"secret-service",
"security-framework",
"tokio",
"whoami",
"windows-sys 0.59.0",
"zeroize",
@ -5889,7 +5997,7 @@ dependencies = [
"serde",
"serde-untagged",
"thiserror 2.0.12",
"toml_edit",
"toml_edit 0.23.2",
"tracing",
"url",
"uv-cache-key",
@ -6071,7 +6179,7 @@ dependencies = [
"tokio",
"tokio-stream",
"toml",
"toml_edit",
"toml_edit 0.23.2",
"tracing",
"url",
"uv-cache-key",
@ -6208,7 +6316,7 @@ dependencies = [
"serde",
"thiserror 2.0.12",
"toml",
"toml_edit",
"toml_edit 0.23.2",
"tracing",
"uv-cache",
"uv-configuration",
@ -6338,7 +6446,7 @@ dependencies = [
"thiserror 2.0.12",
"tokio",
"toml",
"toml_edit",
"toml_edit 0.23.2",
"tracing",
"uv-build-backend",
"uv-cache-key",
@ -7114,6 +7222,61 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zbus"
version = "5.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597f45e98bc7e6f0988276012797855613cd8269e23b5be62cc4e5d28b7e515d"
dependencies = [
"async-broadcast",
"async-recursion",
"async-trait",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"nix 0.30.1",
"ordered-stream",
"serde",
"serde_repr",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.59.0",
"winnow",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c8e4e14dcdd9d97a98b189cd1220f30e8394ad271e8c987da84f73693862c2"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
dependencies = [
"serde",
"static_assertions",
"winnow",
"zvariant",
]
[[package]]
name = "zerocopy"
version = "0.8.26"
@ -7260,3 +7423,44 @@ dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zvariant"
version = "5.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34"
dependencies = [
"proc-macro2",
"quote",
"serde",
"static_assertions",
"syn",
"winnow",
]

View File

@ -15,18 +15,20 @@ default = ["apple-native", "secret-service", "windows-native"]
## Use the built-in Keychain Services on macOS and iOS
apple-native = ["dep:security-framework"]
## Use the secret-service on *nix.
secret-service = ["dep:dbus-secret-service"]
secret-service = ["dep:secret-service"]
## Use the built-in credential store on Windows
windows-native = ["dep:windows-sys", "dep:byteorder"]
[dependencies]
async-trait = { workspace = true }
log = "0.4"
tokio = { workspace = true }
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
security-framework = { version = "3", optional = true }
[target.'cfg(any(target_os = "linux",target_os = "freebsd", target_os = "openbsd"))'.dependencies]
dbus-secret-service = { version = "4", features = ["crypto-rust"], optional = true }
secret-service = { version = "5.0.0", features = ["rt-tokio-crypto-rust"], optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
byteorder = { version = "1", optional = true }
@ -50,6 +52,7 @@ env_logger = "0.11.5"
fastrand = "2"
rpassword = "7"
rprompt = "2"
tokio = { workspace = true }
whoami = "1.5"
[package.metadata.docs.rs]

View File

@ -15,10 +15,10 @@ use keyring::{Entry, Result};
fn main() -> Result<()> {
let entry = Entry::new("my-service", "my-name")?;
entry.set_password("topS3cr3tP4$$w0rd")?;
let password = entry.get_password()?;
entry.set_password("topS3cr3tP4$$w0rd").await?;
let password = entry.get_password().await?;
println!("My password is '{}'", password);
entry.delete_credential()?;
entry.delete_credential().await?;
Ok(())
}
```

View File

@ -5,7 +5,8 @@ use std::collections::HashMap;
use uv_keyring::{Entry, Error, Result};
fn main() {
#[tokio::main(flavor = "current_thread")]
async fn main() {
let mut args: Cli = Cli::parse();
if args.user.eq_ignore_ascii_case("<logged-in username>") {
args.user = whoami::username()
@ -24,11 +25,11 @@ fn main() {
Command::Set { .. } => {
let value = args.get_password_and_attributes();
match &value {
Value::Secret(secret) => match entry.set_secret(secret) {
Value::Secret(secret) => match entry.set_secret(secret).await {
Ok(()) => args.success_message_for(&value),
Err(err) => args.error_message_for(err),
},
Value::Password(password) => match entry.set_password(password) {
Value::Password(password) => match entry.set_password(password).await {
Ok(()) => args.success_message_for(&value),
Err(err) => args.error_message_for(err),
},
@ -37,7 +38,7 @@ fn main() {
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
match entry.update_attributes(&attrs) {
match entry.update_attributes(&attrs).await {
Ok(()) => args.success_message_for(&value),
Err(err) => args.error_message_for(err),
}
@ -45,28 +46,28 @@ fn main() {
_ => panic!("Can't set without a value"),
}
}
Command::Password => match entry.get_password() {
Command::Password => match entry.get_password().await {
Ok(password) => {
println!("{password}");
args.success_message_for(&Value::Password(password));
}
Err(err) => args.error_message_for(err),
},
Command::Secret => match entry.get_secret() {
Command::Secret => match entry.get_secret().await {
Ok(secret) => {
println!("{}", secret_string(&secret));
args.success_message_for(&Value::Secret(secret));
args.success_message_for(&Value::Secret(secret.to_vec()));
}
Err(err) => args.error_message_for(err),
},
Command::Attributes => match entry.get_attributes() {
Command::Attributes => match entry.get_attributes().await {
Ok(attributes) => {
println!("{}", attributes_string(&attributes));
args.success_message_for(&Value::Attributes(attributes));
}
Err(err) => args.error_message_for(err),
},
Command::Delete => match entry.delete_credential() {
Command::Delete => match entry.delete_credential().await {
Ok(()) => args.success_message_for(&Value::None),
Err(err) => args.error_message_for(err),
},

View File

@ -2,14 +2,18 @@ use uv_keyring::{Entry, Error};
#[unsafe(no_mangle)]
extern "C" fn test() {
test_invalid_parameter();
test_empty_keyring();
test_empty_password_input();
test_round_trip_ascii_password();
test_round_trip_non_ascii_password();
test_update_password();
#[cfg(target_os = "ios")]
test_get_credential();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async {
test_invalid_parameter();
test_empty_keyring().await;
test_empty_password_input().await;
test_round_trip_ascii_password().await;
test_round_trip_non_ascii_password().await;
test_update_password().await;
});
}
fn test_invalid_parameter() {
@ -30,87 +34,68 @@ fn test_invalid_parameter() {
);
}
fn test_empty_keyring() {
async fn test_empty_keyring() {
let name = "test_empty_keyring".to_string();
let entry = Entry::new(&name, &name).expect("Failed to create entry");
assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)))
}
fn test_empty_password_input() {
async fn test_empty_password_input() {
let name = "test_empty_password_input".to_string();
let entry = Entry::new(&name, &name).expect("Failed to create entry");
let in_pass = "";
entry
.set_password(in_pass)
.await
.expect("Couldn't set empty password");
let out_pass = entry.get_password().expect("Couldn't get empty password");
let out_pass = entry
.get_password()
.await
.expect("Couldn't get empty password");
assert_eq!(in_pass, out_pass);
entry
.delete_credential()
.await
.expect("Couldn't delete credential with empty password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted password"
)
}
fn test_round_trip_ascii_password() {
async fn test_round_trip_ascii_password() {
let name = "test_round_trip_ascii_password".to_string();
let entry = Entry::new(&name, &name).expect("Failed to create entry");
let password = "test ascii password";
entry.set_password(password).unwrap();
let stored_password = entry.get_password().unwrap();
entry.set_password(password).await.unwrap();
let stored_password = entry.get_password().await.unwrap();
assert_eq!(stored_password, password);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
entry.delete_credential().await.unwrap();
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)))
}
fn test_round_trip_non_ascii_password() {
async fn test_round_trip_non_ascii_password() {
let name = "test_round_trip_non_ascii_password".to_string();
let entry = Entry::new(&name, &name).expect("Failed to create entry");
let password = "このきれいな花は桜です";
entry.set_password(password).unwrap();
let stored_password = entry.get_password().unwrap();
entry.set_password(password).await.unwrap();
let stored_password = entry.get_password().await.unwrap();
assert_eq!(stored_password, password);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
entry.delete_credential().await.unwrap();
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)))
}
fn test_update_password() {
async fn test_update_password() {
let name = "test_update_password".to_string();
let entry = Entry::new(&name, &name).expect("Failed to create entry");
let password = "test ascii password";
entry.set_password(password).unwrap();
let stored_password = entry.get_password().unwrap();
entry.set_password(password).await.unwrap();
let stored_password = entry.get_password().await.unwrap();
assert_eq!(stored_password, password);
let password = "このきれいな花は桜です";
entry.set_password(password).unwrap();
let stored_password = entry.get_password().unwrap();
entry.set_password(password).await.unwrap();
let stored_password = entry.get_password().await.unwrap();
assert_eq!(stored_password, password);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
}
#[cfg(target_os = "ios")]
fn test_get_credential() {
use keyring::ios::IosCredential;
let name = "test_get_credential".to_string();
let entry = Entry::new(&name, &name).expect("Can't create entry for get_credential");
let credential: &IosCredential = entry
.get_credential()
.downcast_ref()
.expect("Not an iOS credential");
assert!(
credential.get_credential().is_err(),
"Platform credential shouldn't exist yet!"
);
entry
.set_password("test get password for get_credential")
.expect("Can't get password for get_credential");
assert!(credential.get_credential().is_ok());
entry.delete_credential().unwrap();
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Platform credential exists after delete password"
)
entry.delete_credential().await.unwrap();
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)))
}

View File

@ -13,35 +13,35 @@ use std::collections::HashMap;
use crate::Result;
/// The API that [credentials](Credential) implement.
#[async_trait::async_trait]
pub trait CredentialApi {
/// Set the credential's password (a string).
///
/// This will persist the password in the underlying store.
fn set_password(&self, password: &str) -> Result<()> {
self.set_secret(password.as_bytes())
async fn set_password(&self, password: &str) -> Result<()> {
self.set_secret(password.as_bytes()).await
}
/// Set the credential's secret (a byte array).
///
/// This will persist the secret in the underlying store.
fn set_secret(&self, password: &[u8]) -> Result<()>;
async fn set_secret(&self, password: &[u8]) -> Result<()>;
/// Retrieve the password (a string) from the underlying credential.
///
/// This has no effect on the underlying store. If there is no credential
/// for this entry, a [NoEntry](crate::Error::NoEntry) error is returned.
fn get_password(&self) -> Result<String> {
let secret = self.get_secret()?;
super::error::decode_password(secret)
async fn get_password(&self) -> Result<String> {
let secret = self.get_secret().await?;
crate::error::decode_password(secret)
}
/// Retrieve a secret (a byte array) from the credential.
///
/// This has no effect on the underlying store. If there is no credential
/// for this entry, a [NoEntry](crate::Error::NoEntry) error is returned.
fn get_secret(&self) -> Result<Vec<u8>>;
async fn get_secret(&self) -> Result<Vec<u8>>;
/// Get the secure store attributes on this entry's credential.
///
@ -53,9 +53,9 @@ pub trait CredentialApi {
///
/// We provide a default (no-op) implementation of this method
/// for backward compatibility with stores that don't implement it.
fn get_attributes(&self) -> Result<HashMap<String, String>> {
async fn get_attributes(&self) -> Result<HashMap<String, String>> {
// this should err in the same cases as get_secret, so first call that for effect
self.get_secret()?;
self.get_secret().await?;
// if we got this far, return success with no attributes
Ok(HashMap::new())
}
@ -71,9 +71,9 @@ pub trait CredentialApi {
///
/// We provide a default no-op implementation of this method
/// for backward compatibility with stores that don't implement it.
fn update_attributes(&self, _: &HashMap<&str, &str>) -> Result<()> {
async fn update_attributes(&self, _: &HashMap<&str, &str>) -> Result<()> {
// this should err in the same cases as get_secret, so first call that for effect
self.get_secret()?;
self.get_secret().await?;
// if we got this far, return success after setting no attributes
Ok(())
}
@ -83,7 +83,7 @@ pub trait CredentialApi {
/// This is not idempotent if the credential existed!
/// A second call to delete_credential will return
/// a [NoEntry](crate::Error::NoEntry) error.
fn delete_credential(&self) -> Result<()>;
async fn delete_credential(&self) -> Result<()>;
/// Return the underlying concrete object cast to [Any].
///

View File

@ -1,285 +0,0 @@
/*!
# iOS Keychain credential store
All credentials on iOS are stored in secure stores called _keychains_.
On iOS there is only one of these, and it has no name. The target
attribute of an [Entry](crate::Entry), for consistency with macOS,
determines which keychain an entry's credential is created in
searched for. On iOS, then, entries must have no target or use
the specially named target `default`.
For a given service/user pair, this module creates/searches for a credential
in the target keychain whose _account_ attribute holds the user
and whose _name_ attribute holds the service.
Because of a quirk in the iOS keychain services API, neither the _account_
nor the _name_ may be the empty string. (Empty strings are treated as
wildcards when looking up credentials by attribute value.)
Credentials on iOS can have a large number of _key/value_ attributes,
but this module controls the _account_ and _name_ attributes and
ignores all the others. so clients can't use it to access or update any attributes.
*/
use security_framework::base::Error;
use security_framework::passwords::{
delete_generic_password, get_generic_password, set_generic_password,
};
use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
use crate::error::{Error as ErrorCode, Result, decode_password};
/// The representation of a generic Keychain credential.
///
/// The actual credentials can have lots of attributes
/// not represented here. There's no way to use this
/// module to get at those attributes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IosCredential {
pub service: String,
pub account: String,
}
impl CredentialApi for IosCredential {
/// Create and write a credential with password for this entry.
///
/// The new credential replaces any existing one in the store.
/// Since there is only one credential with a given _account_ and _user_
/// in any given keychain, there is no chance of ambiguity.
fn set_password(&self, password: &str) -> Result<()> {
self.set_secret(password.as_bytes())?;
Ok(())
}
/// Create and write a credential with secret for this entry.
///
/// The new credential replaces any existing one in the store.
/// Since there is only one credential with a given _account_ and _user_
/// in any given keychain, there is no chance of ambiguity.
fn set_secret(&self, secret: &[u8]) -> Result<()> {
set_generic_password(&self.service, &self.account, secret).map_err(decode_error)?;
Ok(())
}
/// Look up the password for this entry, if any.
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn get_password(&self) -> Result<String> {
let password_bytes = self.get_secret()?;
decode_password(password_bytes)
}
/// Look up the secret for this entry, if any.
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn get_secret(&self) -> Result<Vec<u8>> {
get_generic_password(&self.service, &self.account).map_err(decode_error)
}
/// Delete the underlying generic credential for this entry, if any.
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn delete_credential(&self) -> Result<()> {
delete_generic_password(&self.service, &self.account).map_err(decode_error)?;
Ok(())
}
/// Return the underlying concrete object with an `Any` type so that it can
/// be downgraded to an [IosCredential] for platform-specific processing.
fn as_any(&self) -> &dyn std::any::Any {
self
}
/// Expose the concrete debug formatter for use via the [Credential] trait
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
impl IosCredential {
/// Construct a credential from the underlying generic credential.
///
/// On iOS, this is basically a no-op, because we represent any attributes
/// other than the ones we use to find the generic credential.
/// But at least this checks whether the underlying credential exists.
pub fn get_credential(&self) -> Result<Self> {
get_generic_password(&self.service, &self.account).map_err(decode_error)?;
Ok(self.clone())
}
/// Create a credential representing an iOS keychain entry.
///
/// The target string is ignored, because there's only one keychain.
///
/// Creating a credential does not put anything into the keychain.
/// The keychain entry will be created
/// when [set_password](IosCredential::set_password) is
/// called.
///
/// This will fail if the service or user strings are empty,
/// because empty attribute values act as wildcards in the
/// Keychain Services API.
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
if service.is_empty() {
return Err(ErrorCode::Invalid(
"service".to_string(),
"cannot be empty".to_string(),
));
}
if user.is_empty() {
return Err(ErrorCode::Invalid(
"user".to_string(),
"cannot be empty".to_string(),
));
}
if let Some(target) = target {
if !target.eq_ignore_ascii_case("default") {
return Err(ErrorCode::Invalid(
"target".to_string(),
"only 'default' is allowed".to_string(),
));
}
}
Ok(Self {
service: service.to_string(),
account: user.to_string(),
})
}
}
/// The builder for iOS keychain credentials
pub struct IosCredentialBuilder {}
/// Returns an instance of the iOS credential builder.
///
/// On iOS,
/// this is called once when an entry is first created.
pub fn default_credential_builder() -> Box<CredentialBuilder> {
Box::new(IosCredentialBuilder {})
}
impl CredentialBuilderApi for IosCredentialBuilder {
/// Build an [IosCredential] for the given target, service, and user.
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
Ok(Box::new(IosCredential::new_with_target(
target, service, user,
)?))
}
/// Return the underlying builder object with an `Any` type so that it can
/// be downgraded to an [IosCredentialBuilder] for platform-specific processing.
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
/// Map an iOS API error to a crate error with appropriate annotation
///
/// The iOS error code values used here are from
/// [this reference](https://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-78/lib/SecBase.h.auto.html)
fn decode_error(err: Error) -> ErrorCode {
match err.code() {
-25291 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecNotAvailable
-25292 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecReadOnly
-25300 => ErrorCode::NoEntry, // errSecItemNotFound
_ => ErrorCode::PlatformFailure(Box::new(err)),
}
}
#[cfg(test)]
mod tests {
use super::{IosCredential, default_credential_builder};
use crate::credential::CredentialPersistence;
use crate::{Entry, Error, tests::generate_random_string};
#[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(IosCredential::new_with_target, service, user)
}
#[test]
fn test_invalid_parameter() {
let credential = IosCredential::new_with_target(None, "", "user");
assert!(
matches!(credential, Err(Error::Invalid(_, _))),
"Created credential with empty service"
);
let credential = IosCredential::new_with_target(None, "service", "");
assert!(
matches!(credential, Err(Error::Invalid(_, _))),
"Created entry with empty user"
);
let credential = IosCredential::new_with_target(Some(""), "service", "user");
assert!(
matches!(credential, Err(Error::Invalid(_, _))),
"Created entry with empty target"
);
}
#[test]
fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new);
}
#[test]
fn test_empty_password() {
crate::tests::test_empty_password(entry_new);
}
#[test]
fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new);
}
#[test]
fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new);
}
#[test]
fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new);
}
#[test]
fn test_update() {
crate::tests::test_update(entry_new);
}
#[test]
fn test_get_credential() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let credential: &IosCredential = entry
.get_credential()
.downcast_ref()
.expect("Not a mac credential");
assert!(
credential.get_credential().is_err(),
"Platform credential shouldn't exist yet!"
);
entry
.set_password("test get_credential")
.expect("Can't set password for get_credential");
assert!(credential.get_credential().is_ok());
entry
.delete_credential()
.expect("Couldn't delete after get_credential");
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
}
#[test]
fn test_get_update_attributes() {
crate::tests::test_noop_get_update_attributes(entry_new);
}
}

View File

@ -189,10 +189,6 @@ pub mod secret_service;
#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
pub mod macos;
#[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "apple-native"))]
#[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))]
pub mod ios;
//
// pick the Windows keystore
//
@ -313,9 +309,9 @@ impl Entry {
/// that matches this entry. This can only happen
/// on some platforms, and then only if a third-party
/// application wrote the ambiguous credential.
pub fn set_password(&self, password: &str) -> Result<()> {
pub async fn set_password(&self, password: &str) -> Result<()> {
debug!("set password for entry {:?}", self.inner);
self.inner.set_password(password)
self.inner.set_password(password).await
}
/// Set the secret for this entry.
@ -325,9 +321,9 @@ impl Entry {
/// that matches this entry. This can only happen
/// on some platforms, and then only if a third-party
/// application wrote the ambiguous credential.
pub fn set_secret(&self, secret: &[u8]) -> Result<()> {
pub async fn set_secret(&self, secret: &[u8]) -> Result<()> {
debug!("set secret for entry {:?}", self.inner);
self.inner.set_secret(secret)
self.inner.set_secret(secret).await
}
/// Retrieve the password saved for this entry.
@ -339,9 +335,9 @@ impl Entry {
/// that matches this entry. This can only happen
/// on some platforms, and then only if a third-party
/// application wrote the ambiguous credential.
pub fn get_password(&self) -> Result<String> {
pub async fn get_password(&self) -> Result<String> {
debug!("get password from entry {:?}", self.inner);
self.inner.get_password()
self.inner.get_password().await
}
/// Retrieve the secret saved for this entry.
@ -353,9 +349,9 @@ impl Entry {
/// that matches this entry. This can only happen
/// on some platforms, and then only if a third-party
/// application wrote the ambiguous credential.
pub fn get_secret(&self) -> Result<Vec<u8>> {
pub async fn get_secret(&self) -> Result<Vec<u8>> {
debug!("get secret from entry {:?}", self.inner);
self.inner.get_secret()
self.inner.get_secret().await
}
/// Get the attributes on the underlying credential for this entry.
@ -371,9 +367,9 @@ impl Entry {
/// that matches this entry. This can only happen
/// on some platforms, and then only if a third-party
/// application wrote the ambiguous credential.
pub fn get_attributes(&self) -> Result<HashMap<String, String>> {
pub async fn get_attributes(&self) -> Result<HashMap<String, String>> {
debug!("get attributes from entry {:?}", self.inner);
self.inner.get_attributes()
self.inner.get_attributes().await
}
/// Update the attributes on the underlying credential for this entry.
@ -391,12 +387,12 @@ impl Entry {
/// that matches this entry. This can only happen
/// on some platforms, and then only if a third-party
/// application wrote the ambiguous credential.
pub fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
pub async fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
debug!(
"update attributes for entry {:?} from map {attributes:?}",
self.inner
);
self.inner.update_attributes(attributes)
self.inner.update_attributes(attributes).await
}
/// Delete the underlying credential for this entry.
@ -412,9 +408,9 @@ impl Entry {
/// Note: This does _not_ affect the lifetime of the [Entry]
/// structure, which is controlled by Rust. It only
/// affects the underlying credential store.
pub fn delete_credential(&self) -> Result<()> {
pub async fn delete_credential(&self) -> Result<()> {
debug!("delete entry {:?}", self.inner);
self.inner.delete_credential()
self.inner.delete_credential().await
}
/// Return a reference to this entry's wrapped credential.
@ -474,12 +470,14 @@ mod tests {
}
}
fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
async fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
entry
.set_password(in_pass)
.await
.unwrap_or_else(|err| panic!("Can't set password for {case}: {err:?}"));
let out_pass = entry
.get_password()
.await
.unwrap_or_else(|err| panic!("Can't get password for {case}: {err:?}"));
assert_eq!(
in_pass, out_pass,
@ -488,12 +486,13 @@ mod tests {
}
/// A basic round-trip unit test given an entry and a password.
pub(crate) fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
test_round_trip_no_delete(case, entry, in_pass);
pub(crate) async fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
test_round_trip_no_delete(case, entry, in_pass).await;
entry
.delete_credential()
.await
.unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
let password = entry.get_password();
let password = entry.get_password().await;
assert!(
matches!(password, Err(Error::NoEntry)),
"Read deleted password for {case}",
@ -501,12 +500,14 @@ mod tests {
}
/// A basic round-trip unit test given an entry and a password.
pub(crate) fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
pub(crate) async fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
entry
.set_secret(in_secret)
.await
.unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
let out_secret = entry
.get_secret()
.await
.unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
assert_eq!(
in_secret, &out_secret,
@ -514,8 +515,9 @@ mod tests {
);
entry
.delete_credential()
.await
.unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
let password = entry.get_secret();
let password = entry.get_secret().await;
assert!(
matches!(password, Err(Error::NoEntry)),
"Read deleted password for {case}",
@ -551,101 +553,104 @@ mod tests {
let name = generate_random_string();
let entry = f(&name, &name);
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Missing entry has password"
)
}
pub(crate) fn test_empty_password<F>(f: F)
pub(crate) async fn test_empty_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("empty password", &entry, "");
test_round_trip("empty password", &entry, "").await;
}
pub(crate) fn test_round_trip_ascii_password<F>(f: F)
pub(crate) async fn test_round_trip_ascii_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("ascii password", &entry, "test ascii password");
test_round_trip("ascii password", &entry, "test ascii password").await;
}
pub(crate) fn test_round_trip_non_ascii_password<F>(f: F)
pub(crate) async fn test_round_trip_non_ascii_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です").await;
}
pub(crate) fn test_round_trip_random_secret<F>(f: F)
pub(crate) async fn test_round_trip_random_secret<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
let secret = generate_random_bytes_of_len(24);
test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
test_round_trip_secret("non-ascii password", &entry, secret.as_slice()).await;
}
pub(crate) fn test_update<F>(f: F)
pub(crate) async fn test_update<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
test_round_trip_no_delete("initial ascii password", &entry, "test ascii password").await;
test_round_trip(
"updated non-ascii password",
&entry,
"このきれいな花は桜です",
);
)
.await;
}
pub(crate) fn test_noop_get_update_attributes<F>(f: F)
pub(crate) async fn test_noop_get_update_attributes<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
"Read missing credential in attribute test",
);
let mut map: HashMap<&str, &str> = HashMap::new();
map.insert("test attribute name", "test attribute value");
assert!(
matches!(entry.update_attributes(&map), Err(Error::NoEntry)),
matches!(entry.update_attributes(&map).await, Err(Error::NoEntry)),
"Updated missing credential in attribute test",
);
// create the credential and test again
entry
.set_password("test password for attributes")
.await
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
match entry.get_attributes() {
match entry.get_attributes().await {
Err(err) => panic!("Couldn't get attributes: {err:?}"),
Ok(attrs) if attrs.is_empty() => {}
Ok(attrs) => panic!("Unexpected attributes: {attrs:?}"),
}
assert!(
matches!(entry.update_attributes(&map), Ok(())),
matches!(entry.update_attributes(&map).await, Ok(())),
"Couldn't update attributes in attribute test",
);
match entry.get_attributes() {
match entry.get_attributes().await {
Err(err) => panic!("Couldn't get attributes after update: {err:?}"),
Ok(attrs) if attrs.is_empty() => {}
Ok(attrs) => panic!("Unexpected attributes after update: {attrs:?}"),
}
entry
.delete_credential()
.await
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
"Read deleted credential in attribute test",
);
}

View File

@ -36,7 +36,6 @@ ignores all the others. so clients can't use it to access or update any attribut
*/
use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
use crate::error::{Error as ErrorCode, Result, decode_password};
use crate::ios::IosCredential;
use security_framework::base::Error;
use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain};
use security_framework::os::macos::passwords::find_generic_password;
@ -53,13 +52,14 @@ pub struct MacCredential {
pub account: String,
}
#[async_trait::async_trait]
impl CredentialApi for MacCredential {
/// Create and write a credential with password for this entry.
///
/// The new credential replaces any existing one in the store.
/// Since there is only one credential with a given _account_ and _user_
/// in any given keychain, there is no chance of ambiguity.
fn set_password(&self, password: &str) -> Result<()> {
async fn set_password(&self, password: &str) -> Result<()> {
get_keychain(self)?
.set_generic_password(&self.service, &self.account, password.as_bytes())
.map_err(decode_error)?;
@ -71,7 +71,7 @@ impl CredentialApi for MacCredential {
/// The new credential replaces any existing one in the store.
/// Since there is only one credential with a given _account_ and _user_
/// in any given keychain, there is no chance of ambiguity.
fn set_secret(&self, secret: &[u8]) -> Result<()> {
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
get_keychain(self)?
.set_generic_password(&self.service, &self.account, secret)
.map_err(decode_error)?;
@ -82,7 +82,7 @@ impl CredentialApi for MacCredential {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn get_password(&self) -> Result<String> {
async fn get_password(&self) -> Result<String> {
let (password_bytes, _) =
find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account)
.map_err(decode_error)?;
@ -93,7 +93,7 @@ impl CredentialApi for MacCredential {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn get_secret(&self) -> Result<Vec<u8>> {
async fn get_secret(&self) -> Result<Vec<u8>> {
let (password_bytes, _) =
find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account)
.map_err(decode_error)?;
@ -104,7 +104,7 @@ impl CredentialApi for MacCredential {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn delete_credential(&self) -> Result<()> {
async fn delete_credential(&self) -> Result<()> {
let (_, item) =
find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account)
.map_err(decode_error)?;
@ -199,16 +199,11 @@ impl CredentialBuilderApi for MacCredentialBuilder {
} else {
MacKeychainDomain::User
};
match domain {
MacKeychainDomain::Protected => Ok(Box::new(IosCredential::new_with_target(
None, service, user,
)?)),
_ => Ok(Box::new(MacCredential::new_with_target(
Some(domain),
service,
user,
)?)),
}
Ok(Box::new(MacCredential::new_with_target(
Some(domain),
service,
user,
)?))
}
/// Return the underlying builder object with an `Any` type so that it can
@ -331,38 +326,38 @@ mod tests {
);
}
#[test]
fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new);
#[tokio::test]
async fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new).await;
}
#[test]
fn test_empty_password() {
crate::tests::test_empty_password(entry_new);
#[tokio::test]
async fn test_empty_password() {
crate::tests::test_empty_password(entry_new).await;
}
#[test]
fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new);
#[tokio::test]
async fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new).await;
}
#[test]
fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new);
#[tokio::test]
async fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
}
#[test]
fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new);
#[tokio::test]
async fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new).await;
}
#[test]
fn test_update() {
crate::tests::test_update(entry_new);
#[tokio::test]
async fn test_update() {
crate::tests::test_update(entry_new).await;
}
#[test]
fn test_get_credential() {
#[tokio::test]
async fn test_get_credential() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let credential: &MacCredential = entry
@ -375,17 +370,19 @@ mod tests {
);
entry
.set_password("test get_credential")
.await
.expect("Can't set password for get_credential");
assert!(credential.get_credential().is_ok());
entry
.delete_credential()
.await
.expect("Couldn't delete after get_credential");
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)));
}
#[test]
fn test_get_update_attributes() {
crate::tests::test_noop_get_update_attributes(entry_new);
#[tokio::test]
async fn test_get_update_attributes() {
crate::tests::test_noop_get_update_attributes(entry_new).await;
}
#[test]
@ -405,14 +402,5 @@ mod tests {
)
}
}
for name in ["data protection", "protected"] {
let cred = Entry::new_with_target(name, name, name)
.expect("couldn't create credential")
.inner;
let _: &super::IosCredential = cred
.as_any()
.downcast_ref()
.expect("credential not an iOS credential");
}
}
}

View File

@ -71,13 +71,14 @@ pub struct MockData {
pub error: Option<Error>,
}
#[async_trait::async_trait]
impl CredentialApi for MockCredential {
/// Set a password on a mock credential.
///
/// If there is an error in the mock, it will be returned
/// and the password will _not_ be set. The error will
/// be cleared, so calling again will set the password.
fn set_password(&self, password: &str) -> Result<()> {
async fn set_password(&self, password: &str) -> Result<()> {
let mut inner = self.inner.lock().expect("Can't access mock data for set");
let data = inner.get_mut();
let err = data.error.take();
@ -95,7 +96,7 @@ impl CredentialApi for MockCredential {
/// If there is an error in the mock, it will be returned
/// and the password will _not_ be set. The error will
/// be cleared, so calling again will set the password.
fn set_secret(&self, secret: &[u8]) -> Result<()> {
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
let mut inner = self.inner.lock().expect("Can't access mock data for set");
let data = inner.get_mut();
let err = data.error.take();
@ -112,7 +113,7 @@ impl CredentialApi for MockCredential {
///
/// If there is an error set in the mock, it will
/// be returned instead of a password.
fn get_password(&self) -> Result<String> {
async fn get_password(&self) -> Result<String> {
let mut inner = self.inner.lock().expect("Can't access mock data for get");
let data = inner.get_mut();
let err = data.error.take();
@ -129,7 +130,7 @@ impl CredentialApi for MockCredential {
///
/// If there is an error set in the mock, it will
/// be returned instead of a password.
fn get_secret(&self) -> Result<Vec<u8>> {
async fn get_secret(&self) -> Result<Vec<u8>> {
let mut inner = self.inner.lock().expect("Can't access mock data for get");
let data = inner.get_mut();
let err = data.error.take();
@ -149,7 +150,7 @@ impl CredentialApi for MockCredential {
///
/// If there is no password, a [NoEntry](Error::NoEntry) error
/// will be returned.
fn delete_credential(&self) -> Result<()> {
async fn delete_credential(&self) -> Result<()> {
let mut inner = self
.inner
.lock()
@ -253,43 +254,43 @@ mod tests {
Entry::new_with_credential(Box::new(credential))
}
#[test]
fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new);
#[tokio::test]
async fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new).await;
}
#[test]
fn test_empty_password() {
crate::tests::test_empty_password(entry_new);
#[tokio::test]
async fn test_empty_password() {
crate::tests::test_empty_password(entry_new).await;
}
#[test]
fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new);
#[tokio::test]
async fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new).await;
}
#[test]
fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new);
#[tokio::test]
async fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
}
#[test]
fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new);
#[tokio::test]
async fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new).await;
}
#[test]
fn test_update() {
crate::tests::test_update(entry_new);
#[tokio::test]
async fn test_update() {
crate::tests::test_update(entry_new).await;
}
#[test]
fn test_get_update_attributes() {
crate::tests::test_noop_get_update_attributes(entry_new);
#[tokio::test]
async fn test_get_update_attributes() {
crate::tests::test_noop_get_update_attributes(entry_new).await;
}
#[test]
fn test_set_error() {
#[tokio::test]
async fn test_set_error() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let password = "test ascii password";
@ -303,32 +304,37 @@ mod tests {
"is an error".to_string(),
));
assert!(
matches!(entry.set_password(password), Err(Error::Invalid(_, _))),
matches!(
entry.set_password(password).await,
Err(Error::Invalid(_, _))
),
"set: No error"
);
entry
.set_password(password)
.await
.expect("set: Error not cleared");
mock.set_error(Error::NoEntry);
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"get: No error"
);
let stored_password = entry.get_password().expect("get: Error not cleared");
let stored_password = entry.get_password().await.expect("get: Error not cleared");
assert_eq!(
stored_password, password,
"Retrieved and set ascii passwords don't match"
);
mock.set_error(Error::TooLong("mock".to_string(), 3));
assert!(
matches!(entry.delete_credential(), Err(Error::TooLong(_, 3))),
matches!(entry.delete_credential().await, Err(Error::TooLong(_, 3))),
"delete: No error"
);
entry
.delete_credential()
.await
.expect("delete: Error not cleared");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
)
}

View File

@ -80,7 +80,7 @@ issue for more details and possible workarounds.
*/
use std::collections::HashMap;
use dbus_secret_service::{Collection, EncryptionType, Error, Item, SecretService};
use secret_service::{Collection, EncryptionType, Error, Item, SecretService};
use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
use crate::error::{Error as ErrorCode, Result, decode_password};
@ -100,31 +100,38 @@ pub struct SsCredential {
target: Option<String>,
}
#[async_trait::async_trait]
impl CredentialApi for SsCredential {
/// Sets the password on a unique matching item, if it exists, or creates one if necessary.
///
/// If there are multiple matches,
/// returns an [Ambiguous](ErrorCode::Ambiguous) error with a credential for each
/// matching item.
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous) error with
/// a credential for each matching item.
///
/// When creating, the item is put into a collection named by the credential's `target`
/// attribute.
fn set_password(&self, password: &str) -> Result<()> {
self.set_secret(password.as_bytes())
async fn set_password(&self, password: &str) -> Result<()> {
self.set_secret(password.as_bytes()).await
}
/// Sets the secret on a unique matching item, if it exists, or creates one if necessary.
///
/// If there are multiple matches,
/// returns an [Ambiguous](ErrorCode::Ambiguous) error with a credential for each
/// matching item.
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous) error
/// with a credential for each matching item.
///
/// When creating, the item is put into a collection named by the credential's `target`
/// attribute.
fn set_secret(&self, secret: &[u8]) -> Result<()> {
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
// first try to find a unique, existing, matching item and set its password
match self.map_matching_items(|i| set_item_secret(i, secret), true) {
Ok(_) => return Ok(()),
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.matching_items(&ss).await {
Ok(items) => {
for item in items.iter() {
set_item_secret(item, secret).await?;
}
return Ok(());
}
Err(ErrorCode::NoEntry) => {}
Err(err) => return Err(err),
}
@ -132,9 +139,14 @@ impl CredentialApi for SsCredential {
// an item, the credential must have an explicit target. All entries created with
// the [new] or [new_with_target] commands will have explicit targets. But entries
// created to wrap 3rd-party items that don't have `target` attributes may not.
let ss = SecretService::connect(EncryptionType::Dh).map_err(platform_failure)?;
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
let name = self.target.as_ref().ok_or_else(empty_target)?;
let collection = get_collection(&ss, name).or_else(|_| create_collection(&ss, name))?;
let collection = match get_collection(&ss, name).await {
Ok(collection) => collection,
Err(_) => create_collection(&ss, name).await?,
};
collection
.create_item(
self.label.as_str(),
@ -143,19 +155,29 @@ impl CredentialApi for SsCredential {
true, // replace
"text/plain",
)
.await
.map_err(platform_failure)?;
Ok(())
}
/// Gets the password on a unique matching item, if it exists.
///
/// If there are no
/// matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
/// If there are multiple matches,
/// returns an [Ambiguous](ErrorCode::Ambiguous)
/// If there are no matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous)
/// error with a credential for each matching item.
fn get_password(&self) -> Result<String> {
Ok(self.map_matching_items(get_item_password, true)?.remove(0))
async fn get_password(&self) -> Result<String> {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.unique_matching_item(&ss).await {
Ok(item) => get_item_password(&item).await,
Err(ErrorCode::NoEntry) => {
let collection = ss.get_default_collection().await.map_err(decode_error)?;
let item = self.find_unique_legacy_item(&collection).await?;
get_item_password(&item).await
}
Err(err) => Err(err),
}
}
/// Gets the secret on a unique matching item, if it exists.
@ -165,33 +187,71 @@ impl CredentialApi for SsCredential {
/// If there are multiple matches,
/// returns an [Ambiguous](ErrorCode::Ambiguous)
/// error with a credential for each matching item.
fn get_secret(&self) -> Result<Vec<u8>> {
Ok(self.map_matching_items(get_item_secret, true)?.remove(0))
async fn get_secret(&self) -> Result<Vec<u8>> {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.unique_matching_item(&ss).await {
Ok(item) => get_item_secret(&item).await,
Err(ErrorCode::NoEntry) => {
let collection = ss.get_default_collection().await.map_err(decode_error)?;
let item = self.find_unique_legacy_item(&collection).await?;
get_item_secret(&item).await
}
Err(err) => Err(err),
}
}
/// Get attributes on a unique matching item, if it exists
fn get_attributes(&self) -> Result<HashMap<String, String>> {
let attributes: Vec<HashMap<String, String>> =
self.map_matching_items(get_item_attributes, true)?;
Ok(attributes.into_iter().next().unwrap())
async fn get_attributes(&self) -> Result<HashMap<String, String>> {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.unique_matching_item(&ss).await {
Ok(item) => get_item_attributes(&item).await,
Err(ErrorCode::NoEntry) => {
let collection = ss.get_default_collection().await.map_err(decode_error)?;
let item = self.find_unique_legacy_item(&collection).await?;
get_item_attributes(&item).await
}
Err(err) => Err(err),
}
}
/// Update attributes on a unique matching item, if it exists
fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
self.map_matching_items(|i| update_item_attributes(i, attributes), true)?;
Ok(())
async fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.unique_matching_item(&ss).await {
Ok(item) => update_item_attributes(&item, attributes).await,
Err(ErrorCode::NoEntry) => {
let collection = ss.get_default_collection().await.map_err(decode_error)?;
let item = self.find_unique_legacy_item(&collection).await?;
update_item_attributes(&item, attributes).await
}
Err(err) => Err(err),
}
}
/// Deletes the unique matching item, if it exists.
///
/// If there are no
/// matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
/// If there are multiple matches,
/// returns an [Ambiguous](ErrorCode::Ambiguous)
/// If there are no matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous)
/// error with a credential for each matching item.
fn delete_credential(&self) -> Result<()> {
self.map_matching_items(delete_item, true)?;
Ok(())
async fn delete_credential(&self) -> Result<()> {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.unique_matching_item(&ss).await {
Ok(item) => delete_item(&item).await,
Err(ErrorCode::NoEntry) => {
let collection = ss.get_default_collection().await.map_err(decode_error)?;
let item = self.find_unique_legacy_item(&collection).await?;
delete_item(&item).await
}
Err(err) => Err(err),
}
}
/// Return the underlying credential object with an `Any` type so that it can
@ -230,7 +290,7 @@ impl SsCredential {
Ok(Self {
attributes,
label: format!(
"{user}@{service}:{target} (keyring v{})",
"{user}@{service}:{target} (uv v{})",
env!("CARGO_PKG_VERSION"),
),
target: Some(target.to_string()),
@ -250,7 +310,7 @@ impl SsCredential {
Ok(Self {
attributes,
label: format!(
"keyring-rs v{} for no target, service '{service}', user '{user}'",
"uv v{} for no target, service '{service}', user '{user}'",
env!("CARGO_PKG_VERSION"),
),
target: None,
@ -261,90 +321,137 @@ impl SsCredential {
///
/// The created credential will have all the attributes and label
/// of the underlying item, so you can examine them.
pub fn new_from_item(item: &Item) -> Result<Self> {
let attributes = item.get_attributes().map_err(decode_error)?;
pub async fn new_from_item(item: &Item<'_>) -> Result<Self> {
let attributes = item.get_attributes().await.map_err(decode_error)?;
let target = attributes.get("target").cloned();
Ok(Self {
attributes,
label: item.get_label().map_err(decode_error)?,
label: item.get_label().await.map_err(decode_error)?,
target,
})
}
/// Construct a credential for this credential's underlying matching item,
/// if there is exactly one.
pub fn new_from_matching_item(&self) -> Result<Self> {
Ok(self
.map_matching_items(Self::new_from_item, true)?
.remove(0))
pub async fn new_from_matching_item(&self) -> Result<Self> {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.unique_matching_item(&ss).await {
Ok(item) => Self::new_from_item(&item).await,
Err(ErrorCode::NoEntry) => {
let collection = ss.get_default_collection().await.map_err(decode_error)?;
let item = self.find_unique_legacy_item(&collection).await?;
Self::new_from_item(&item).await
}
Err(err) => Err(err),
}
}
/// If there are multiple matching items for this credential, get all of their passwords.
///
/// (This is useful if [get_password](SsCredential::get_password)
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
pub fn get_all_passwords(&self) -> Result<Vec<String>> {
self.map_matching_items(get_item_password, false)
pub async fn get_all_passwords(&self) -> Result<Vec<String>> {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.matching_items(&ss).await {
Ok(items) => {
let mut passwords = Vec::new();
for item in items.iter() {
passwords.push(get_item_password(item).await?);
}
Ok(passwords)
}
Err(ErrorCode::NoEntry) => {
let collection = ss.get_default_collection().await.map_err(decode_error)?;
let items = self.find_legacy_items(&collection).await?;
let mut passwords = Vec::new();
for item in items.iter() {
passwords.push(get_item_password(item).await?);
}
Ok(passwords)
}
Err(err) => Err(err),
}
}
/// If there are multiple matching items for this credential, delete all of them.
///
/// (This is useful if [delete_credential](SsCredential::delete_credential)
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
pub fn delete_all_passwords(&self) -> Result<()> {
self.map_matching_items(delete_item, false)?;
Ok(())
pub async fn delete_all_passwords(&self) -> Result<()> {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.map_err(platform_failure)?;
match self.matching_items(&ss).await {
Ok(items) => {
for item in items.iter() {
delete_item(item).await?;
}
Ok(())
}
Err(ErrorCode::NoEntry) => {
let collection = ss.get_default_collection().await.map_err(decode_error)?;
let items = self.find_legacy_items(&collection).await?;
for item in items.iter() {
delete_item(item).await?;
}
Ok(())
}
Err(err) => Err(err),
}
}
/// Map a function over the items matching this credential.
/// Find and unlock items matching this credential.
///
/// Items are unlocked before the function is applied.
///
/// If `require_unique` is true, and there are no matching items, then
/// a [NoEntry](ErrorCode::NoEntry) error is returned.
/// If `require_unique` is true, and there are multiple matches,
/// then an [Ambiguous](ErrorCode::Ambiguous) error is returned
/// with a vector containing one
/// credential for each of the matching items.
pub fn map_matching_items<F, T>(&self, f: F, require_unique: bool) -> Result<Vec<T>>
where
F: Fn(&Item) -> Result<T>,
T: Sized,
{
let ss = SecretService::connect(EncryptionType::Dh).map_err(platform_failure)?;
/// Items are unlocked before being returned.
async fn matching_items<'a>(&self, ss: &'a SecretService<'_>) -> Result<Vec<Item<'a>>> {
let attributes: HashMap<&str, &str> = self.search_attributes(false).into_iter().collect();
let search = ss.search_items(attributes).map_err(decode_error)?;
let search = ss.search_items(attributes).await.map_err(decode_error)?;
let count = search.locked.len() + search.unlocked.len();
if count == 0 {
if let Some("default") = self.target.as_deref() {
return self.map_matching_legacy_items(&ss, f, require_unique);
}
return Err(ErrorCode::NoEntry);
}
if require_unique {
if count == 0 {
return Err(ErrorCode::NoEntry);
} else if count > 1 {
let mut results: Vec<Item<'a>> = vec![];
for item in search.unlocked {
results.push(item);
}
for item in search.locked {
item.unlock().await.map_err(decode_error)?;
results.push(item);
}
Ok(results)
}
/// Find and unlock a unique item matching this credential.
///
/// If there are no matching items, then a [NoEntry](ErrorCode::NoEntry) error is returned.
/// If there are multiple matches, then an [Ambiguous](ErrorCode::Ambiguous) error is
/// returned with a vector containing one credential for each of the matching items.
async fn unique_matching_item<'a>(&self, ss: &'a SecretService<'_>) -> Result<Item<'a>> {
let mut items = self.matching_items(ss).await?;
match items.len() {
0 => Err(ErrorCode::NoEntry),
1 => Ok(items.pop().unwrap()),
_ => {
let mut creds: Vec<Box<Credential>> = vec![];
let attributes: HashMap<&str, &str> =
self.search_attributes(false).into_iter().collect();
let search = ss.search_items(attributes).await.map_err(decode_error)?;
for item in search.locked.iter().chain(search.unlocked.iter()) {
let cred = Self::new_from_item(item)?;
let cred = Self::new_from_item(item).await?;
creds.push(Box::new(cred))
}
return Err(ErrorCode::Ambiguous(creds));
}
}
let mut results: Vec<T> = vec![];
for item in search.unlocked.iter() {
results.push(f(item)?);
}
for item in search.locked.iter() {
item.unlock().map_err(decode_error)?;
results.push(f(item)?);
}
Ok(results)
}
/// Map a function over items that older versions of keyring
/// would have matched against this credential.
/// Find legacy items in the given collection if the credential being searched has
/// the default target.
///
/// Keyring v1 created secret service items that had no target attribute, and it was
/// only able to create items in the default collection. Keyring v2, and Keyring v3.1,
@ -362,40 +469,59 @@ impl SsCredential {
/// collection.
///
/// So with keyring v3.2.1, if the service-wide search fails to find any matching
/// credential, and the credential being searched for has the default target (or
/// no target), we fall back and search the default collection for a v1-style credential.
/// credential, and the credential being searched for has the default target,
/// we fall back and search the default collection for a v1-style credential.
/// That preserves the legacy behavior at the cost of a second round-trip through
/// the secret service for the collection search.
pub fn map_matching_legacy_items<F, T>(
&self,
ss: &SecretService,
f: F,
require_unique: bool,
) -> Result<Vec<T>>
where
F: Fn(&Item) -> Result<T>,
T: Sized,
{
let collection = ss.get_default_collection().map_err(decode_error)?;
let attributes = self.search_attributes(true);
let search = collection.search_items(attributes).map_err(decode_error)?;
if require_unique {
if search.is_empty() && require_unique {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there are no matching items or
/// if the credential being searched does not have the default target.
async fn find_legacy_items<'a>(&self, collection: &'a Collection<'_>) -> Result<Vec<Item<'a>>> {
if let Some("default") = self.target.as_deref() {
let attributes = self.search_attributes(true);
let search = collection
.search_items(attributes)
.await
.map_err(decode_error)?;
if search.is_empty() {
return Err(ErrorCode::NoEntry);
} else if search.len() > 1 {
}
Ok(search)
} else {
Err(ErrorCode::NoEntry)
}
}
/// Find unique legacy item in the given collection if the credential being searched has
/// the default target.
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there are no matching items or
/// if the credential being searched does not have the default target.
/// If there are multiple matches, then an [Ambiguous](ErrorCode::Ambiguous) error is
/// returned with a vector containing one credential for each of the matching items.
async fn find_unique_legacy_item<'a>(
&self,
collection: &'a Collection<'_>,
) -> Result<Item<'a>> {
let mut items = self.find_legacy_items(collection).await?;
match items.len() {
0 => Err(ErrorCode::NoEntry),
1 => Ok(items.pop().unwrap()),
_ => {
let mut creds: Vec<Box<Credential>> = vec![];
let attributes = self.search_attributes(true);
let search = collection
.search_items(attributes)
.await
.map_err(decode_error)?;
for item in search.iter() {
let cred = Self::new_from_item(item)?;
let cred = Self::new_from_item(item).await?;
creds.push(Box::new(cred))
}
return Err(ErrorCode::Ambiguous(creds));
}
}
let mut results: Vec<T> = vec![];
for item in search.iter() {
results.push(f(item)?);
}
Ok(results)
}
/// Using strings in the credential map makes managing the lifetime
@ -457,18 +583,23 @@ impl CredentialBuilderApi for SsCredentialBuilder {
///
/// The name `default` is treated specially and is interpreted as naming
/// the default collection regardless of its label (which might be different).
pub fn get_collection<'a>(ss: &'a SecretService, name: &str) -> Result<Collection<'a>> {
pub async fn get_collection<'a>(ss: &'a SecretService<'_>, name: &str) -> Result<Collection<'a>> {
let collection = if name.eq("default") {
ss.get_default_collection().map_err(decode_error)?
ss.get_default_collection().await.map_err(decode_error)?
} else {
let all = ss.get_all_collections().map_err(decode_error)?;
let found = all
.into_iter()
.find(|c| c.get_label().map(|l| l.eq(name)).unwrap_or(false));
let all = ss.get_all_collections().await.map_err(decode_error)?;
let mut found = None;
for c in all {
let label = c.get_label().await.map_err(decode_error)?;
if label.eq(name) {
found = Some(c);
break;
}
}
found.ok_or(ErrorCode::NoEntry)?
};
if collection.is_locked().map_err(decode_error)? {
collection.unlock().map_err(decode_error)?;
if collection.is_locked().await.map_err(decode_error)? {
collection.unlock().await.map_err(decode_error)?;
}
Ok(collection)
}
@ -478,45 +609,56 @@ pub fn get_collection<'a>(ss: &'a SecretService, name: &str) -> Result<Collectio
/// If a collection with that name already exists, it is returned.
///
/// The name `default` is specially interpreted to mean the default collection.
pub fn create_collection<'a>(ss: &'a SecretService, name: &str) -> Result<Collection<'a>> {
pub async fn create_collection<'a>(
ss: &'a SecretService<'_>,
name: &str,
) -> Result<Collection<'a>> {
let collection = if name.eq("default") {
ss.get_default_collection().map_err(decode_error)?
ss.get_default_collection().await.map_err(decode_error)?
} else {
ss.create_collection(name, "").map_err(decode_error)?
ss.create_collection(name, "").await.map_err(decode_error)?
};
Ok(collection)
}
/// Given an existing item, set its secret.
pub fn set_item_secret(item: &Item, secret: &[u8]) -> Result<()> {
item.set_secret(secret, "text/plain").map_err(decode_error)
pub async fn set_item_secret(item: &Item<'_>, secret: &[u8]) -> Result<()> {
item.set_secret(secret, "text/plain")
.await
.map_err(decode_error)
}
/// Given an existing item, retrieve and decode its password.
pub fn get_item_password(item: &Item) -> Result<String> {
let bytes = item.get_secret().map_err(decode_error)?;
pub async fn get_item_password(item: &Item<'_>) -> Result<String> {
let bytes = item.get_secret().await.map_err(decode_error)?;
decode_password(bytes)
}
/// Given an existing item, retrieve its secret.
pub fn get_item_secret(item: &Item) -> Result<Vec<u8>> {
let secret = item.get_secret().map_err(decode_error)?;
pub async fn get_item_secret(item: &Item<'_>) -> Result<Vec<u8>> {
let secret = item.get_secret().await.map_err(decode_error)?;
Ok(secret)
}
/// Given an existing item, retrieve its non-controlled attributes.
pub fn get_item_attributes(item: &Item) -> Result<HashMap<String, String>> {
let mut attributes = item.get_attributes().map_err(decode_error)?;
pub async fn get_item_attributes(item: &Item<'_>) -> Result<HashMap<String, String>> {
let mut attributes = item.get_attributes().await.map_err(decode_error)?;
attributes.remove("target");
attributes.remove("service");
attributes.remove("username");
attributes.insert("label".to_string(), item.get_label().map_err(decode_error)?);
attributes.insert(
"label".to_string(),
item.get_label().await.map_err(decode_error)?,
);
Ok(attributes)
}
/// Given an existing item, retrieve its non-controlled attributes.
pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) -> Result<()> {
let existing = item.get_attributes().map_err(decode_error)?;
pub async fn update_item_attributes(
item: &Item<'_>,
attributes: &HashMap<&str, &str>,
) -> Result<()> {
let existing = item.get_attributes().await.map_err(decode_error)?;
let mut updated: HashMap<&str, &str> = HashMap::new();
for (k, v) in existing.iter() {
updated.insert(k, v);
@ -532,7 +674,7 @@ pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) ->
"cannot be empty".to_string(),
));
}
item.set_label(v).map_err(decode_error)?;
item.set_label(v).await.map_err(decode_error)?;
if updated.contains_key("label") {
updated.insert("label", v);
}
@ -540,13 +682,13 @@ pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) ->
updated.insert(k, v);
}
}
item.set_attributes(updated).map_err(decode_error)?;
item.set_attributes(updated).await.map_err(decode_error)?;
Ok(())
}
// Given an existing item, delete it.
pub fn delete_item(item: &Item) -> Result<()> {
item.delete().map_err(decode_error)
pub async fn delete_item(item: &Item<'_>) -> Result<()> {
item.delete().await.map_err(decode_error)
}
//
@ -609,47 +751,43 @@ mod tests {
);
}
#[test]
fn test_empty_service_and_user() {
crate::tests::test_empty_service_and_user(entry_new);
}
#[tokio::test]
async fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new).await;
}
#[test]
fn test_empty_password() {
crate::tests::test_empty_password(entry_new);
#[tokio::test]
async fn test_empty_password() {
crate::tests::test_empty_password(entry_new).await;
}
#[test]
fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new);
#[tokio::test]
async fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new).await;
}
#[test]
fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new);
#[tokio::test]
async fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
}
#[test]
fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new);
#[tokio::test]
async fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new).await;
}
#[test]
fn test_update() {
crate::tests::test_update(entry_new);
#[tokio::test]
async fn test_update() {
crate::tests::test_update(entry_new).await;
}
#[test]
fn test_get_credential() {
#[tokio::test]
async fn test_get_credential() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
entry
.set_password("test get credential")
.await
.expect("Can't set password for get_credential");
let credential: &SsCredential = entry
.get_credential()
@ -657,6 +795,7 @@ mod tests {
.expect("Not a secret service credential");
let actual = credential
.new_from_matching_item()
.await
.expect("Can't read credential");
assert_eq!(actual.label, credential.label, "Labels don't match");
for (key, value) in &credential.attributes {
@ -668,19 +807,20 @@ mod tests {
}
entry
.delete_credential()
.await
.expect("Couldn't delete get-credential");
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)));
}
#[test]
fn test_get_update_attributes() {
#[tokio::test]
async fn test_get_update_attributes() {
let name = generate_random_string();
let credential = SsCredential::new_with_target(None, &name, &name)
.expect("Can't create credential for attribute test");
let create_label = credential.label.clone();
let entry = Entry::new_with_credential(Box::new(credential));
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
"Read missing credential in attribute test",
);
let mut in_map: HashMap<&str, &str> = HashMap::new();
@ -690,15 +830,17 @@ mod tests {
in_map.insert("service", "ignored service value");
in_map.insert("username", "ignored username value");
assert!(
matches!(entry.update_attributes(&in_map), Err(Error::NoEntry)),
matches!(entry.update_attributes(&in_map).await, Err(Error::NoEntry)),
"Updated missing credential in attribute test",
);
// create the credential and test again
entry
.set_password("test password for attributes")
.await
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
let out_map = entry
.get_attributes()
.await
.expect("Can't get attributes after create");
assert_eq!(out_map["label"], create_label);
assert_eq!(out_map["application"], "rust-keyring");
@ -706,11 +848,12 @@ mod tests {
assert!(!out_map.contains_key("service"));
assert!(!out_map.contains_key("username"));
assert!(
matches!(entry.update_attributes(&in_map), Ok(())),
matches!(entry.update_attributes(&in_map).await, Ok(())),
"Couldn't update attributes in attribute test",
);
let after_map = entry
.get_attributes()
.await
.expect("Can't get attributes after update");
assert_eq!(after_map["label"], in_map["label"]);
assert_eq!(
@ -720,21 +863,25 @@ mod tests {
assert_eq!(out_map["application"], "rust-keyring");
in_map.insert("label", "");
assert!(
matches!(entry.update_attributes(&in_map), Err(Error::Invalid(_, _))),
matches!(
entry.update_attributes(&in_map).await,
Err(Error::Invalid(_, _))
),
"Was able to set empty label in attribute test",
);
entry
.delete_credential()
.await
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
assert!(
matches!(entry.get_attributes(), Err(Error::NoEntry)),
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
"Read deleted credential in attribute test",
);
}
#[test]
#[tokio::test]
#[ignore = "can't be run headless, because it needs to prompt"]
fn test_create_new_target_collection() {
async fn test_create_new_target_collection() {
let name = generate_random_string();
let credential = SsCredential::new_with_target(Some(&name), &name, &name)
.expect("Can't create credential for new collection");
@ -742,21 +889,24 @@ mod tests {
let password = "password in new collection";
entry
.set_password(password)
.await
.expect("Can't set password for new collection entry");
let actual = entry
.get_password()
.await
.expect("Can't get password for new collection entry");
assert_eq!(actual, password);
entry
.delete_credential()
.await
.expect("Couldn't delete password for new collection entry");
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
delete_collection(&name);
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)));
delete_collection(&name).await;
}
#[test]
#[tokio::test]
#[ignore = "can't be run headless, because it needs to prompt"]
fn test_separate_targets_dont_interfere() {
async fn test_separate_targets_dont_interfere() {
let name1 = generate_random_string();
let name2 = generate_random_string();
let credential1 = SsCredential::new_with_target(Some(&name1), &name1, &name1)
@ -771,72 +921,90 @@ mod tests {
let password3 = "password for default collection";
entry1
.set_password(password1)
.await
.expect("Can't set password for collection 1");
entry2
.set_password(password2)
.await
.expect("Can't set password for collection 2");
entry3
.set_password(password3)
.await
.expect("Can't set password for default collection");
let actual1 = entry1
.get_password()
.await
.expect("Can't get password for collection 1");
assert_eq!(actual1, password1);
let actual2 = entry2
.get_password()
.await
.expect("Can't get password for collection 2");
assert_eq!(actual2, password2);
let actual3 = entry3
.get_password()
.await
.expect("Can't get password for default collection");
assert_eq!(actual3, password3);
entry1
.delete_credential()
.await
.expect("Couldn't delete password for collection 1");
assert!(matches!(entry1.get_password(), Err(Error::NoEntry)));
assert!(matches!(entry1.get_password().await, Err(Error::NoEntry)));
entry2
.delete_credential()
.await
.expect("Couldn't delete password for collection 2");
assert!(matches!(entry2.get_password(), Err(Error::NoEntry)));
assert!(matches!(entry2.get_password().await, Err(Error::NoEntry)));
entry3
.delete_credential()
.await
.expect("Couldn't delete password for default collection");
assert!(matches!(entry3.get_password(), Err(Error::NoEntry)));
delete_collection(&name1);
delete_collection(&name2);
assert!(matches!(entry3.get_password().await, Err(Error::NoEntry)));
delete_collection(&name1).await;
delete_collection(&name2).await;
}
#[test]
fn test_legacy_entry() {
#[tokio::test]
async fn test_legacy_entry() {
let name = generate_random_string();
let pw = "test password";
let v3_entry = Entry::new(&name, &name).expect("Can't create v3 entry");
let _ = v3_entry.get_password().expect_err("Found v3 entry");
create_v1_entry(&name, pw);
let password = v3_entry.get_password().expect("Can't find v1 entry");
let _ = v3_entry.get_password().await.expect_err("Found v3 entry");
create_v1_entry(&name, pw).await;
let password = v3_entry.get_password().await.expect("Can't find v1 entry");
assert_eq!(password, pw);
v3_entry.delete_credential().expect("Can't delete v1 entry");
v3_entry
.delete_credential()
.await
.expect("Can't delete v1 entry");
let _ = v3_entry
.get_password()
.await
.expect_err("Got password for v1 entry after delete");
}
fn delete_collection(name: &str) {
let ss =
SecretService::connect(EncryptionType::Dh).expect("Can't connect to secret service");
let collection = super::get_collection(&ss, name).expect("Can't find collection to delete");
collection.delete().expect("Can't delete collection");
async fn delete_collection(name: &str) {
let ss = SecretService::connect(EncryptionType::Dh)
.await
.expect("Can't connect to secret service");
let collection = super::get_collection(&ss, name)
.await
.expect("Can't find collection to delete");
collection.delete().await.expect("Can't delete collection");
}
fn create_v1_entry(name: &str, password: &str) {
use dbus_secret_service::{EncryptionType, SecretService};
async fn create_v1_entry(name: &str, password: &str) {
use secret_service::{EncryptionType, SecretService};
let cred = SsCredential::new_with_no_target(name, name)
.expect("Can't create credential with no target");
let ss =
SecretService::connect(EncryptionType::Dh).expect("Can't connect to secret service");
let ss = SecretService::connect(EncryptionType::Dh)
.await
.expect("Can't connect to secret service");
let collection = ss
.get_default_collection()
.await
.expect("Can't get default collection");
collection
.create_item(
@ -846,6 +1014,7 @@ mod tests {
true, // replace
"text/plain",
)
.await
.expect("Can't create item with no target in default collection");
}
}

View File

@ -77,7 +77,7 @@ impl CredentialApi for WinCredential {
/// The new credential replaces any existing one in the store.
/// Since there is only one credential with a given _target name_,
/// there is no chance of ambiguity.
fn set_password(&self, password: &str) -> Result<()> {
async fn set_password(&self, password: &str) -> Result<()> {
self.validate_attributes(None, Some(password))?;
// Password strings are converted to UTF-16, because that's the native
// charset for Windows strings. This allows interoperability with native
@ -98,7 +98,7 @@ impl CredentialApi for WinCredential {
/// The new credential replaces any existing one in the store.
/// Since there is only one credential with a given _target name_,
/// there is no chance of ambiguity.
fn set_secret(&self, secret: &[u8]) -> Result<()> {
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
self.validate_attributes(Some(secret), None)?;
self.save_credential(secret)
}
@ -107,7 +107,7 @@ impl CredentialApi for WinCredential {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn get_password(&self) -> Result<String> {
async fn get_password(&self) -> Result<String> {
self.extract_from_platform(extract_password)
}
@ -115,7 +115,7 @@ impl CredentialApi for WinCredential {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn get_secret(&self) -> Result<Vec<u8>> {
async fn get_secret(&self) -> Result<Vec<u8>> {
self.extract_from_platform(extract_secret)
}
@ -123,7 +123,7 @@ impl CredentialApi for WinCredential {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn get_attributes(&self) -> Result<HashMap<String, String>> {
async fn get_attributes(&self) -> Result<HashMap<String, String>> {
let cred = self.extract_from_platform(Self::extract_credential)?;
let mut attributes: HashMap<String, String> = HashMap::new();
attributes.insert("comment".to_string(), cred.comment.clone());
@ -136,7 +136,7 @@ impl CredentialApi for WinCredential {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
async fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
let secret = self.extract_from_platform(extract_secret)?;
let mut cred = self.extract_from_platform(Self::extract_credential)?;
if let Some(comment) = attributes.get(&"comment") {
@ -156,7 +156,7 @@ impl CredentialApi for WinCredential {
///
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
/// credential in the store.
fn delete_credential(&self) -> Result<()> {
async fn delete_credential(&self) -> Result<()> {
self.validate_attributes(None, None)?;
let target_name = to_wstr(&self.target_name);
let cred_type = CRED_TYPE_GENERIC;
@ -528,13 +528,14 @@ mod tests {
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)
}
#[allow(clippy::cast_possible_truncation)]
#[test]
fn test_bad_password() {
fn make_platform_credential(password: &mut Vec<u8>) -> CREDENTIALW {
@ -592,7 +593,7 @@ mod tests {
assert_eq!(val, len, "Error names wrong limit");
}
Err(other) => panic!("Error is not '{attr} too long': {other}"),
Ok(_) => panic!("No error when {attr} too long"),
Ok(()) => panic!("No error when {attr} too long"),
}
}
let cred = WinCredential {
@ -622,11 +623,11 @@ mod tests {
match attr {
"password" => {
let password = generate_random_string_of_len((len / 2) as usize + 1);
validate(bad_cred.validate_attributes(None, Some(&password)))
validate(bad_cred.validate_attributes(None, Some(&password)));
}
"secret" => {
let secret: Vec<u8> = vec![255u8; len as usize + 1];
validate(bad_cred.validate_attributes(Some(&secret), None))
validate(bad_cred.validate_attributes(Some(&secret), None));
}
_ => validate(bad_cred.validate_attributes(None, None)),
}
@ -659,49 +660,44 @@ mod tests {
);
}
#[test]
fn test_empty_service_and_user() {
crate::tests::test_empty_service_and_user(entry_new);
}
#[tokio::test]
async fn test_missing_entry() {
crate::tests::test_missing_entry(entry_new).await;
}
#[test]
fn test_empty_password() {
crate::tests::test_empty_password(entry_new);
#[tokio::test]
async fn test_empty_password() {
crate::tests::test_empty_password(entry_new).await;
}
#[test]
fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new);
#[tokio::test]
async fn test_round_trip_ascii_password() {
crate::tests::test_round_trip_ascii_password(entry_new).await;
}
#[test]
fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new);
#[tokio::test]
async fn test_round_trip_non_ascii_password() {
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
}
#[test]
fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new);
#[tokio::test]
async fn test_round_trip_random_secret() {
crate::tests::test_round_trip_random_secret(entry_new).await;
}
#[test]
fn test_update() {
crate::tests::test_update(entry_new);
#[tokio::test]
async fn test_update() {
crate::tests::test_update(entry_new).await;
}
#[test]
fn test_get_update_attributes() {
#[tokio::test]
async fn test_get_update_attributes() {
let name = generate_random_string();
let cred = WinCredential::new_with_target(None, &name, &name)
.expect("Can't create credential for attribute test");
let entry = Entry::new_with_credential(Box::new(cred.clone()));
assert!(
matches!(entry.get_attributes(), Err(ErrorCode::NoEntry)),
matches!(entry.get_attributes().await, Err(ErrorCode::NoEntry)),
"Read missing credential in attribute test",
);
let mut in_map: HashMap<&str, &str> = HashMap::new();
@ -711,25 +707,31 @@ mod tests {
in_map.insert("comment", "comment value");
in_map.insert("username", "username value");
assert!(
matches!(entry.update_attributes(&in_map), Err(ErrorCode::NoEntry)),
matches!(
entry.update_attributes(&in_map).await,
Err(ErrorCode::NoEntry)
),
"Updated missing credential in attribute test",
);
// create the credential and test again
entry
.set_password("test password for attributes")
.await
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
let out_map = entry
.get_attributes()
.await
.expect("Can't get attributes after create");
assert_eq!(out_map["target_alias"], cred.target_alias);
assert_eq!(out_map["comment"], cred.comment);
assert_eq!(out_map["username"], cred.username);
assert!(
matches!(entry.update_attributes(&in_map), Ok(())),
matches!(entry.update_attributes(&in_map).await, Ok(())),
"Couldn't update attributes in attribute test",
);
let after_map = entry
.get_attributes()
.await
.expect("Can't get attributes after update");
assert_eq!(after_map["target_alias"], in_map["target_alias"]);
assert_eq!(after_map["comment"], in_map["comment"]);
@ -738,20 +740,22 @@ mod tests {
assert!(!after_map.contains_key("attribute name"));
entry
.delete_credential()
.await
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
assert!(
matches!(entry.get_attributes(), Err(ErrorCode::NoEntry)),
matches!(entry.get_attributes().await, Err(ErrorCode::NoEntry)),
"Read deleted credential in attribute test",
);
}
#[test]
fn test_get_credential() {
#[tokio::test]
async fn test_get_credential() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let password = "test get password";
entry
.set_password(password)
.await
.expect("Can't set test get password");
let credential: &WinCredential = entry
.get_credential()
@ -773,7 +777,11 @@ mod tests {
assert_eq!(actual.comment, credential.comment, "Comments don't match");
entry
.delete_credential()
.await
.expect("Couldn't delete get-credential");
assert!(matches!(entry.get_password(), Err(ErrorCode::NoEntry)));
assert!(matches!(
entry.get_password().await,
Err(ErrorCode::NoEntry)
));
}
}

View File

@ -3,21 +3,21 @@ use uv_keyring::{Entry, Error};
mod common;
#[test]
fn test_missing_entry() {
#[tokio::test]
async fn test_missing_entry() {
init_logger();
let name = generate_random_string();
let entry = Entry::new(&name, &name).expect("Can't create entry");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Missing entry has password"
)
}
#[test]
#[tokio::test]
#[cfg(target_os = "linux")]
fn test_empty_password() {
async fn test_empty_password() {
init_logger();
let name = generate_random_string();
@ -25,21 +25,28 @@ fn test_empty_password() {
let in_pass = "";
entry
.set_password(in_pass)
.await
.expect("Can't set empty password");
let out_pass = entry.get_password().expect("Can't get empty password");
let out_pass = entry
.get_password()
.await
.expect("Can't get empty password");
assert_eq!(
in_pass, out_pass,
"Retrieved and set empty passwords don't match"
);
entry.delete_credential().expect("Can't delete password");
entry
.delete_credential()
.await
.expect("Can't delete password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted password"
)
}
#[test]
fn test_round_trip_ascii_password() {
#[tokio::test]
async fn test_round_trip_ascii_password() {
init_logger();
let name = generate_random_string();
@ -47,48 +54,28 @@ fn test_round_trip_ascii_password() {
let password = "test ascii password";
entry
.set_password(password)
.await
.expect("Can't set ascii password");
let stored_password = entry.get_password().expect("Can't get ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
)
}
#[cfg(target_os = "macos")]
#[test]
fn test_round_trip_protected_keychain() {
init_logger();
let name = generate_random_string();
let entry = Entry::new_with_target("protected", &name, &name).expect("Can't create entry");
let password = "test protected ascii password";
entry
.set_password(password)
.expect("Can't set protected ascii password");
let stored_password = entry.get_password().expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set protected ascii passwords don't match"
);
entry
.delete_credential()
.expect("Can't delete protected ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Able to read a deleted protected ascii password"
)
}
#[test]
fn test_round_trip_non_ascii_password() {
#[tokio::test]
async fn test_round_trip_non_ascii_password() {
init_logger();
let name = generate_random_string();
@ -96,23 +83,28 @@ fn test_round_trip_non_ascii_password() {
let password = "このきれいな花は桜です";
entry
.set_password(password)
.await
.expect("Can't set non-ascii password");
let stored_password = entry.get_password().expect("Can't get non-ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get non-ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set non-ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete non-ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted non-ascii password"
)
}
#[test]
fn test_round_trip_random_secret() {
#[tokio::test]
async fn test_round_trip_random_secret() {
init_logger();
let name = generate_random_string();
@ -120,8 +112,9 @@ fn test_round_trip_random_secret() {
let secret = generate_random_bytes_of_len(24);
entry
.set_secret(secret.as_slice())
.await
.expect("Can't set random secret");
let stored_secret = entry.get_secret().expect("Can't get random secret");
let stored_secret = entry.get_secret().await.expect("Can't get random secret");
assert_eq!(
&stored_secret,
secret.as_slice(),
@ -129,15 +122,16 @@ fn test_round_trip_random_secret() {
);
entry
.delete_credential()
.await
.expect("Can't delete random secret");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted random secret"
)
}
#[test]
fn test_update() {
#[tokio::test]
async fn test_update() {
init_logger();
let name = generate_random_string();
@ -145,8 +139,12 @@ fn test_update() {
let password = "test ascii password";
entry
.set_password(password)
.await
.expect("Can't set initial ascii password");
let stored_password = entry.get_password().expect("Can't get ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set initial ascii passwords don't match"
@ -154,17 +152,22 @@ fn test_update() {
let password = "このきれいな花は桜です";
entry
.set_password(password)
.await
.expect("Can't update ascii with non-ascii password");
let stored_password = entry.get_password().expect("Can't get non-ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get non-ascii password");
assert_eq!(
stored_password, password,
"Retrieved and updated non-ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete updated password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted updated password"
)
}

View File

@ -3,45 +3,60 @@ use uv_keyring::{Entry, Error};
mod common;
#[test]
fn test_create_then_move() {
#[tokio::test]
async fn test_create_then_move() {
init_logger();
let name = generate_random_string();
let entry = Entry::new(&name, &name).unwrap();
let test = move || {
let password = "test ascii password";
entry
.set_password(password)
.expect("Can't set initial ascii password");
let stored_password = entry.get_password().expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set initial ascii passwords don't match"
);
let password = "このきれいな花は桜です";
entry
.set_password(password)
.expect("Can't set non-ascii password");
let stored_password = entry.get_password().expect("Can't get non-ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set non-ascii passwords don't match"
);
entry
.delete_credential()
.expect("Can't delete non-ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Able to read a deleted non-ascii password"
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let password = "test ascii password";
entry
.set_password(password)
.await
.expect("Can't set initial ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set initial ascii passwords don't match"
);
let password = "このきれいな花は桜です";
entry
.set_password(password)
.await
.expect("Can't set non-ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get non-ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set non-ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete non-ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted non-ascii password"
);
});
};
let handle = std::thread::spawn(test);
assert!(handle.join().is_ok(), "Couldn't execute on thread")
}
#[test]
fn test_simultaneous_create_then_move() {
#[tokio::test]
async fn test_simultaneous_create_then_move() {
init_logger();
let mut handles = vec![];
@ -49,19 +64,32 @@ fn test_simultaneous_create_then_move() {
let name = format!("{}-{}", generate_random_string(), i);
let entry = Entry::new(&name, &name).expect("Can't create entry");
let test = move || {
entry.set_password(&name).expect("Can't set ascii password");
let stored_password = entry.get_password().expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
});
};
handles.push(std::thread::spawn(test))
}
@ -70,9 +98,9 @@ fn test_simultaneous_create_then_move() {
}
}
#[test]
#[tokio::test]
#[cfg(not(target_os = "windows"))]
fn test_create_set_then_move() {
async fn test_create_set_then_move() {
init_logger();
let name = generate_random_string();
@ -80,138 +108,195 @@ fn test_create_set_then_move() {
let password = "test ascii password";
entry
.set_password(password)
.await
.expect("Can't set ascii password");
let test = move || {
let stored_password = entry.get_password().expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
});
};
let handle = std::thread::spawn(test);
assert!(handle.join().is_ok(), "Couldn't execute on thread")
}
#[test]
#[tokio::test]
#[cfg(not(target_os = "windows"))]
fn test_simultaneous_create_set_then_move() {
async fn test_simultaneous_create_set_then_move() {
init_logger();
let mut handles = vec![];
for i in 0..10 {
let name = format!("{}-{}", generate_random_string(), i);
let entry = Entry::new(&name, &name).expect("Can't create entry");
entry.set_password(&name).expect("Can't set ascii password");
let test = move || {
let stored_password = entry.get_password().expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().expect("Couldn't execute on thread")
}
}
#[test]
fn test_simultaneous_independent_create_set() {
init_logger();
let mut handles = vec![];
for i in 0..10 {
let name = format!("thread_entry{i}");
let test = move || {
let entry = Entry::new(&name, &name).expect("Can't create entry");
entry.set_password(&name).expect("Can't set ascii password");
let stored_password = entry.get_password().expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().expect("Couldn't execute on thread")
}
}
#[test]
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn test_multiple_create_delete_single_thread() {
init_logger();
let name = generate_random_string();
let entry = Entry::new(&name, &name).expect("Can't create entry");
let repeats = 10;
for _i in 0..repeats {
entry.set_password(&name).expect("Can't set ascii password");
let stored_password = entry.get_password().expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
}
}
#[test]
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn test_simultaneous_multiple_create_delete_single_thread() {
init_logger();
let mut handles = vec![];
for t in 0..10 {
let name = generate_random_string();
.set_password(&name)
.await
.expect("Can't set ascii password");
let test = move || {
let name = format!("{name}-{t}");
let entry = Entry::new(&name, &name).expect("Can't create entry");
let repeats = 10;
for _i in 0..repeats {
entry.set_password(&name).expect("Can't set ascii password");
let stored_password = entry.get_password().expect("Can't get ascii password");
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
}
});
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().expect("Couldn't execute on thread")
}
}
#[tokio::test]
async fn test_simultaneous_independent_create_set() {
init_logger();
let mut handles = vec![];
for i in 0..10 {
let name = format!("thread_entry{i}");
let test = move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let entry = Entry::new(&name, &name).expect("Can't create entry");
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
});
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().expect("Couldn't execute on thread")
}
}
#[tokio::test]
#[cfg(any(target_os = "macos", target_os = "windows"))]
async fn test_multiple_create_delete_single_thread() {
init_logger();
let name = generate_random_string();
let entry = Entry::new(&name, &name).expect("Can't create entry");
let repeats = 10;
for _i in 0..repeats {
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
}
}
#[tokio::test]
#[cfg(any(target_os = "macos", target_os = "windows"))]
async fn test_simultaneous_multiple_create_delete_single_thread() {
init_logger();
let mut handles = vec![];
for t in 0..10 {
let name = generate_random_string();
let test = move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let name = format!("{name}-{t}");
let entry = Entry::new(&name, &name).expect("Can't create entry");
let repeats = 10;
for _i in 0..repeats {
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
}
});
};
handles.push(std::thread::spawn(test))
}