From bda4e7bb4fa8f2f196097a48c5e3abd1765087e0 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 16 Jul 2025 11:42:29 +0200 Subject: [PATCH] Switch to secret-service crate and async functions --- Cargo.lock | 298 +++++++++++-- crates/uv-keyring/Cargo.toml | 7 +- crates/uv-keyring/README.md | 6 +- crates/uv-keyring/examples/cli.rs | 19 +- crates/uv-keyring/examples/ios.rs | 93 ++-- crates/uv-keyring/src/credential.rs | 26 +- crates/uv-keyring/src/ios.rs | 285 ------------ crates/uv-keyring/src/lib.rs | 91 ++-- crates/uv-keyring/src/macos.rs | 86 ++-- crates/uv-keyring/src/mock.rs | 72 ++-- crates/uv-keyring/src/secret_service.rs | 551 ++++++++++++++++-------- crates/uv-keyring/src/windows.rs | 88 ++-- crates/uv-keyring/tests/basic.rs | 103 ++--- crates/uv-keyring/tests/threading.rs | 385 ++++++++++------- 14 files changed, 1141 insertions(+), 969 deletions(-) delete mode 100644 crates/uv-keyring/src/ios.rs diff --git a/Cargo.lock b/Cargo.lock index b6cf86f07..088c44fe8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", +] diff --git a/crates/uv-keyring/Cargo.toml b/crates/uv-keyring/Cargo.toml index f8efa5d7c..605664b94 100644 --- a/crates/uv-keyring/Cargo.toml +++ b/crates/uv-keyring/Cargo.toml @@ -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] diff --git a/crates/uv-keyring/README.md b/crates/uv-keyring/README.md index c863618c6..cd379d773 100644 --- a/crates/uv-keyring/README.md +++ b/crates/uv-keyring/README.md @@ -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(()) } ``` diff --git a/crates/uv-keyring/examples/cli.rs b/crates/uv-keyring/examples/cli.rs index 4b55914d2..dee84e450 100644 --- a/crates/uv-keyring/examples/cli.rs +++ b/crates/uv-keyring/examples/cli.rs @@ -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("") { 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), }, diff --git a/crates/uv-keyring/examples/ios.rs b/crates/uv-keyring/examples/ios.rs index dc69cf4f5..72b6329b6 100644 --- a/crates/uv-keyring/examples/ios.rs +++ b/crates/uv-keyring/examples/ios.rs @@ -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))) } diff --git a/crates/uv-keyring/src/credential.rs b/crates/uv-keyring/src/credential.rs index b97b8d0fc..965540d01 100644 --- a/crates/uv-keyring/src/credential.rs +++ b/crates/uv-keyring/src/credential.rs @@ -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 { - let secret = self.get_secret()?; - super::error::decode_password(secret) + async fn get_password(&self) -> Result { + 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>; + async fn get_secret(&self) -> Result>; /// 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> { + async fn get_attributes(&self) -> 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 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]. /// diff --git a/crates/uv-keyring/src/ios.rs b/crates/uv-keyring/src/ios.rs deleted file mode 100644 index 611c62e89..000000000 --- a/crates/uv-keyring/src/ios.rs +++ /dev/null @@ -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 { - 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> { - 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 { - 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 { - 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 { - 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> { - 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); - } -} diff --git a/crates/uv-keyring/src/lib.rs b/crates/uv-keyring/src/lib.rs index 0dd74a68d..07a012bcf 100644 --- a/crates/uv-keyring/src/lib.rs +++ b/crates/uv-keyring/src/lib.rs @@ -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 { + pub async fn get_password(&self) -> Result { 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> { + pub async fn get_secret(&self) -> Result> { 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> { + pub async fn get_attributes(&self) -> Result> { 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) + pub(crate) async fn test_empty_password(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) + pub(crate) async fn test_round_trip_ascii_password(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) + pub(crate) async fn test_round_trip_non_ascii_password(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) + pub(crate) async fn test_round_trip_random_secret(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) + pub(crate) async fn test_update(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) + pub(crate) async fn test_noop_get_update_attributes(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", ); } diff --git a/crates/uv-keyring/src/macos.rs b/crates/uv-keyring/src/macos.rs index d89e5f3e6..45437d91f 100644 --- a/crates/uv-keyring/src/macos.rs +++ b/crates/uv-keyring/src/macos.rs @@ -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 { + async fn get_password(&self) -> Result { 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> { + async fn get_secret(&self) -> Result> { 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"); - } } } diff --git a/crates/uv-keyring/src/mock.rs b/crates/uv-keyring/src/mock.rs index 2a70397a9..d9111ec2c 100644 --- a/crates/uv-keyring/src/mock.rs +++ b/crates/uv-keyring/src/mock.rs @@ -71,13 +71,14 @@ pub struct MockData { pub error: Option, } +#[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 { + async fn get_password(&self) -> Result { 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> { + async fn get_secret(&self) -> Result> { 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" ) } diff --git a/crates/uv-keyring/src/secret_service.rs b/crates/uv-keyring/src/secret_service.rs index bd0f0d927..7bbb3acb6 100644 --- a/crates/uv-keyring/src/secret_service.rs +++ b/crates/uv-keyring/src/secret_service.rs @@ -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, } +#[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 { - Ok(self.map_matching_items(get_item_password, true)?.remove(0)) + async fn get_password(&self) -> Result { + 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> { - Ok(self.map_matching_items(get_item_secret, true)?.remove(0)) + async fn get_secret(&self) -> Result> { + 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> { - let attributes: Vec> = - self.map_matching_items(get_item_attributes, true)?; - Ok(attributes.into_iter().next().unwrap()) + async fn get_attributes(&self) -> Result> { + 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 { - let attributes = item.get_attributes().map_err(decode_error)?; + pub async fn new_from_item(item: &Item<'_>) -> Result { + 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 { - Ok(self - .map_matching_items(Self::new_from_item, true)? - .remove(0)) + pub async fn new_from_matching_item(&self) -> Result { + 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> { - self.map_matching_items(get_item_password, false) + pub async fn get_all_passwords(&self) -> Result> { + 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(&self, f: F, require_unique: bool) -> Result> - where - F: Fn(&Item) -> Result, - 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>> { 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> = 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> { + let mut items = self.matching_items(ss).await?; + match items.len() { + 0 => Err(ErrorCode::NoEntry), + 1 => Ok(items.pop().unwrap()), + _ => { let mut creds: Vec> = 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 = 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( - &self, - ss: &SecretService, - f: F, - require_unique: bool, - ) -> Result> - where - F: Fn(&Item) -> Result, - 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>> { + 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> { + 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> = 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 = 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> { +pub async fn get_collection<'a>(ss: &'a SecretService<'_>, name: &str) -> Result> { 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(ss: &'a SecretService, name: &str) -> Result> { +pub async fn create_collection<'a>( + ss: &'a SecretService<'_>, + name: &str, +) -> Result> { 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 { - let bytes = item.get_secret().map_err(decode_error)?; +pub async fn get_item_password(item: &Item<'_>) -> Result { + 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> { - let secret = item.get_secret().map_err(decode_error)?; +pub async fn get_item_secret(item: &Item<'_>) -> Result> { + 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> { - let mut attributes = item.get_attributes().map_err(decode_error)?; +pub async fn get_item_attributes(item: &Item<'_>) -> Result> { + 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"); } } diff --git a/crates/uv-keyring/src/windows.rs b/crates/uv-keyring/src/windows.rs index 4e4b2b181..e374ed9b5 100644 --- a/crates/uv-keyring/src/windows.rs +++ b/crates/uv-keyring/src/windows.rs @@ -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 { + async fn get_password(&self) -> Result { 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> { + async fn get_secret(&self) -> Result> { 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> { + async fn get_attributes(&self) -> Result> { let cred = self.extract_from_platform(Self::extract_credential)?; let mut attributes: HashMap = 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) -> 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 = 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) + )); } } diff --git a/crates/uv-keyring/tests/basic.rs b/crates/uv-keyring/tests/basic.rs index d5bf87729..9f9b5cd42 100644 --- a/crates/uv-keyring/tests/basic.rs +++ b/crates/uv-keyring/tests/basic.rs @@ -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" ) } diff --git a/crates/uv-keyring/tests/threading.rs b/crates/uv-keyring/tests/threading.rs index 9496c8fd2..7a610bacf 100644 --- a/crates/uv-keyring/tests/threading.rs +++ b/crates/uv-keyring/tests/threading.rs @@ -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)) }