mirror of https://github.com/astral-sh/uv
Switch to secret-service crate and async functions
This commit is contained in:
parent
68b6a6f14b
commit
bda4e7bb4f
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ use std::collections::HashMap;
|
|||
|
||||
use uv_keyring::{Entry, Error, Result};
|
||||
|
||||
fn main() {
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let mut args: Cli = Cli::parse();
|
||||
if args.user.eq_ignore_ascii_case("<logged-in username>") {
|
||||
args.user = whoami::username()
|
||||
|
|
@ -24,11 +25,11 @@ fn main() {
|
|||
Command::Set { .. } => {
|
||||
let value = args.get_password_and_attributes();
|
||||
match &value {
|
||||
Value::Secret(secret) => match entry.set_secret(secret) {
|
||||
Value::Secret(secret) => match entry.set_secret(secret).await {
|
||||
Ok(()) => args.success_message_for(&value),
|
||||
Err(err) => args.error_message_for(err),
|
||||
},
|
||||
Value::Password(password) => match entry.set_password(password) {
|
||||
Value::Password(password) => match entry.set_password(password).await {
|
||||
Ok(()) => args.success_message_for(&value),
|
||||
Err(err) => args.error_message_for(err),
|
||||
},
|
||||
|
|
@ -37,7 +38,7 @@ fn main() {
|
|||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
match entry.update_attributes(&attrs) {
|
||||
match entry.update_attributes(&attrs).await {
|
||||
Ok(()) => args.success_message_for(&value),
|
||||
Err(err) => args.error_message_for(err),
|
||||
}
|
||||
|
|
@ -45,28 +46,28 @@ fn main() {
|
|||
_ => panic!("Can't set without a value"),
|
||||
}
|
||||
}
|
||||
Command::Password => match entry.get_password() {
|
||||
Command::Password => match entry.get_password().await {
|
||||
Ok(password) => {
|
||||
println!("{password}");
|
||||
args.success_message_for(&Value::Password(password));
|
||||
}
|
||||
Err(err) => args.error_message_for(err),
|
||||
},
|
||||
Command::Secret => match entry.get_secret() {
|
||||
Command::Secret => match entry.get_secret().await {
|
||||
Ok(secret) => {
|
||||
println!("{}", secret_string(&secret));
|
||||
args.success_message_for(&Value::Secret(secret));
|
||||
args.success_message_for(&Value::Secret(secret.to_vec()));
|
||||
}
|
||||
Err(err) => args.error_message_for(err),
|
||||
},
|
||||
Command::Attributes => match entry.get_attributes() {
|
||||
Command::Attributes => match entry.get_attributes().await {
|
||||
Ok(attributes) => {
|
||||
println!("{}", attributes_string(&attributes));
|
||||
args.success_message_for(&Value::Attributes(attributes));
|
||||
}
|
||||
Err(err) => args.error_message_for(err),
|
||||
},
|
||||
Command::Delete => match entry.delete_credential() {
|
||||
Command::Delete => match entry.delete_credential().await {
|
||||
Ok(()) => args.success_message_for(&Value::None),
|
||||
Err(err) => args.error_message_for(err),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,35 +13,35 @@ use std::collections::HashMap;
|
|||
|
||||
use crate::Result;
|
||||
|
||||
|
||||
/// The API that [credentials](Credential) implement.
|
||||
#[async_trait::async_trait]
|
||||
pub trait CredentialApi {
|
||||
/// Set the credential's password (a string).
|
||||
///
|
||||
/// This will persist the password in the underlying store.
|
||||
fn set_password(&self, password: &str) -> Result<()> {
|
||||
self.set_secret(password.as_bytes())
|
||||
async fn set_password(&self, password: &str) -> Result<()> {
|
||||
self.set_secret(password.as_bytes()).await
|
||||
}
|
||||
|
||||
/// Set the credential's secret (a byte array).
|
||||
///
|
||||
/// This will persist the secret in the underlying store.
|
||||
fn set_secret(&self, password: &[u8]) -> Result<()>;
|
||||
async fn set_secret(&self, password: &[u8]) -> Result<()>;
|
||||
|
||||
/// Retrieve the password (a string) from the underlying credential.
|
||||
///
|
||||
/// This has no effect on the underlying store. If there is no credential
|
||||
/// for this entry, a [NoEntry](crate::Error::NoEntry) error is returned.
|
||||
fn get_password(&self) -> Result<String> {
|
||||
let secret = self.get_secret()?;
|
||||
super::error::decode_password(secret)
|
||||
async fn get_password(&self) -> Result<String> {
|
||||
let secret = self.get_secret().await?;
|
||||
crate::error::decode_password(secret)
|
||||
}
|
||||
|
||||
/// Retrieve a secret (a byte array) from the credential.
|
||||
///
|
||||
/// This has no effect on the underlying store. If there is no credential
|
||||
/// for this entry, a [NoEntry](crate::Error::NoEntry) error is returned.
|
||||
fn get_secret(&self) -> Result<Vec<u8>>;
|
||||
async fn get_secret(&self) -> Result<Vec<u8>>;
|
||||
|
||||
/// Get the secure store attributes on this entry's credential.
|
||||
///
|
||||
|
|
@ -53,9 +53,9 @@ pub trait CredentialApi {
|
|||
///
|
||||
/// We provide a default (no-op) implementation of this method
|
||||
/// for backward compatibility with stores that don't implement it.
|
||||
fn get_attributes(&self) -> Result<HashMap<String, String>> {
|
||||
async fn get_attributes(&self) -> Result<HashMap<String, String>> {
|
||||
// this should err in the same cases as get_secret, so first call that for effect
|
||||
self.get_secret()?;
|
||||
self.get_secret().await?;
|
||||
// if we got this far, return success with no attributes
|
||||
Ok(HashMap::new())
|
||||
}
|
||||
|
|
@ -71,9 +71,9 @@ pub trait CredentialApi {
|
|||
///
|
||||
/// We provide a default no-op implementation of this method
|
||||
/// for backward compatibility with stores that don't implement it.
|
||||
fn update_attributes(&self, _: &HashMap<&str, &str>) -> Result<()> {
|
||||
async fn update_attributes(&self, _: &HashMap<&str, &str>) -> Result<()> {
|
||||
// this should err in the same cases as get_secret, so first call that for effect
|
||||
self.get_secret()?;
|
||||
self.get_secret().await?;
|
||||
// if we got this far, return success after setting no attributes
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ pub trait CredentialApi {
|
|||
/// This is not idempotent if the credential existed!
|
||||
/// A second call to delete_credential will return
|
||||
/// a [NoEntry](crate::Error::NoEntry) error.
|
||||
fn delete_credential(&self) -> Result<()>;
|
||||
async fn delete_credential(&self) -> Result<()>;
|
||||
|
||||
/// Return the underlying concrete object cast to [Any].
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,285 +0,0 @@
|
|||
/*!
|
||||
|
||||
# iOS Keychain credential store
|
||||
|
||||
All credentials on iOS are stored in secure stores called _keychains_.
|
||||
On iOS there is only one of these, and it has no name. The target
|
||||
attribute of an [Entry](crate::Entry), for consistency with macOS,
|
||||
determines which keychain an entry's credential is created in
|
||||
searched for. On iOS, then, entries must have no target or use
|
||||
the specially named target `default`.
|
||||
|
||||
For a given service/user pair, this module creates/searches for a credential
|
||||
in the target keychain whose _account_ attribute holds the user
|
||||
and whose _name_ attribute holds the service.
|
||||
Because of a quirk in the iOS keychain services API, neither the _account_
|
||||
nor the _name_ may be the empty string. (Empty strings are treated as
|
||||
wildcards when looking up credentials by attribute value.)
|
||||
|
||||
Credentials on iOS can have a large number of _key/value_ attributes,
|
||||
but this module controls the _account_ and _name_ attributes and
|
||||
ignores all the others. so clients can't use it to access or update any attributes.
|
||||
*/
|
||||
|
||||
use security_framework::base::Error;
|
||||
use security_framework::passwords::{
|
||||
delete_generic_password, get_generic_password, set_generic_password,
|
||||
};
|
||||
|
||||
use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
|
||||
use crate::error::{Error as ErrorCode, Result, decode_password};
|
||||
|
||||
/// The representation of a generic Keychain credential.
|
||||
///
|
||||
/// The actual credentials can have lots of attributes
|
||||
/// not represented here. There's no way to use this
|
||||
/// module to get at those attributes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IosCredential {
|
||||
pub service: String,
|
||||
pub account: String,
|
||||
}
|
||||
|
||||
impl CredentialApi for IosCredential {
|
||||
/// Create and write a credential with password for this entry.
|
||||
///
|
||||
/// The new credential replaces any existing one in the store.
|
||||
/// Since there is only one credential with a given _account_ and _user_
|
||||
/// in any given keychain, there is no chance of ambiguity.
|
||||
fn set_password(&self, password: &str) -> Result<()> {
|
||||
self.set_secret(password.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create and write a credential with secret for this entry.
|
||||
///
|
||||
/// The new credential replaces any existing one in the store.
|
||||
/// Since there is only one credential with a given _account_ and _user_
|
||||
/// in any given keychain, there is no chance of ambiguity.
|
||||
fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
set_generic_password(&self.service, &self.account, secret).map_err(decode_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up the password for this entry, if any.
|
||||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn get_password(&self) -> Result<String> {
|
||||
let password_bytes = self.get_secret()?;
|
||||
decode_password(password_bytes)
|
||||
}
|
||||
|
||||
/// Look up the secret for this entry, if any.
|
||||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
get_generic_password(&self.service, &self.account).map_err(decode_error)
|
||||
}
|
||||
|
||||
/// Delete the underlying generic credential for this entry, if any.
|
||||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn delete_credential(&self) -> Result<()> {
|
||||
delete_generic_password(&self.service, &self.account).map_err(decode_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the underlying concrete object with an `Any` type so that it can
|
||||
/// be downgraded to an [IosCredential] for platform-specific processing.
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
/// Expose the concrete debug formatter for use via the [Credential] trait
|
||||
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl IosCredential {
|
||||
/// Construct a credential from the underlying generic credential.
|
||||
///
|
||||
/// On iOS, this is basically a no-op, because we represent any attributes
|
||||
/// other than the ones we use to find the generic credential.
|
||||
/// But at least this checks whether the underlying credential exists.
|
||||
pub fn get_credential(&self) -> Result<Self> {
|
||||
get_generic_password(&self.service, &self.account).map_err(decode_error)?;
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
/// Create a credential representing an iOS keychain entry.
|
||||
///
|
||||
/// The target string is ignored, because there's only one keychain.
|
||||
///
|
||||
/// Creating a credential does not put anything into the keychain.
|
||||
/// The keychain entry will be created
|
||||
/// when [set_password](IosCredential::set_password) is
|
||||
/// called.
|
||||
///
|
||||
/// This will fail if the service or user strings are empty,
|
||||
/// because empty attribute values act as wildcards in the
|
||||
/// Keychain Services API.
|
||||
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
|
||||
if service.is_empty() {
|
||||
return Err(ErrorCode::Invalid(
|
||||
"service".to_string(),
|
||||
"cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if user.is_empty() {
|
||||
return Err(ErrorCode::Invalid(
|
||||
"user".to_string(),
|
||||
"cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(target) = target {
|
||||
if !target.eq_ignore_ascii_case("default") {
|
||||
return Err(ErrorCode::Invalid(
|
||||
"target".to_string(),
|
||||
"only 'default' is allowed".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
service: service.to_string(),
|
||||
account: user.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The builder for iOS keychain credentials
|
||||
pub struct IosCredentialBuilder {}
|
||||
|
||||
/// Returns an instance of the iOS credential builder.
|
||||
///
|
||||
/// On iOS,
|
||||
/// this is called once when an entry is first created.
|
||||
pub fn default_credential_builder() -> Box<CredentialBuilder> {
|
||||
Box::new(IosCredentialBuilder {})
|
||||
}
|
||||
|
||||
impl CredentialBuilderApi for IosCredentialBuilder {
|
||||
/// Build an [IosCredential] for the given target, service, and user.
|
||||
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
|
||||
Ok(Box::new(IosCredential::new_with_target(
|
||||
target, service, user,
|
||||
)?))
|
||||
}
|
||||
|
||||
/// Return the underlying builder object with an `Any` type so that it can
|
||||
/// be downgraded to an [IosCredentialBuilder] for platform-specific processing.
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an iOS API error to a crate error with appropriate annotation
|
||||
///
|
||||
/// The iOS error code values used here are from
|
||||
/// [this reference](https://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-78/lib/SecBase.h.auto.html)
|
||||
fn decode_error(err: Error) -> ErrorCode {
|
||||
match err.code() {
|
||||
-25291 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecNotAvailable
|
||||
-25292 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecReadOnly
|
||||
-25300 => ErrorCode::NoEntry, // errSecItemNotFound
|
||||
_ => ErrorCode::PlatformFailure(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IosCredential, default_credential_builder};
|
||||
use crate::credential::CredentialPersistence;
|
||||
use crate::{Entry, Error, tests::generate_random_string};
|
||||
|
||||
#[test]
|
||||
fn test_persistence() {
|
||||
assert!(matches!(
|
||||
default_credential_builder().persistence(),
|
||||
CredentialPersistence::UntilDelete
|
||||
))
|
||||
}
|
||||
|
||||
fn entry_new(service: &str, user: &str) -> Entry {
|
||||
crate::tests::entry_from_constructor(IosCredential::new_with_target, service, user)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_parameter() {
|
||||
let credential = IosCredential::new_with_target(None, "", "user");
|
||||
assert!(
|
||||
matches!(credential, Err(Error::Invalid(_, _))),
|
||||
"Created credential with empty service"
|
||||
);
|
||||
let credential = IosCredential::new_with_target(None, "service", "");
|
||||
assert!(
|
||||
matches!(credential, Err(Error::Invalid(_, _))),
|
||||
"Created entry with empty user"
|
||||
);
|
||||
let credential = IosCredential::new_with_target(Some(""), "service", "user");
|
||||
assert!(
|
||||
matches!(credential, Err(Error::Invalid(_, _))),
|
||||
"Created entry with empty target"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_entry() {
|
||||
crate::tests::test_missing_entry(entry_new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
crate::tests::test_update(entry_new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_credential() {
|
||||
let name = generate_random_string();
|
||||
let entry = entry_new(&name, &name);
|
||||
let credential: &IosCredential = entry
|
||||
.get_credential()
|
||||
.downcast_ref()
|
||||
.expect("Not a mac credential");
|
||||
assert!(
|
||||
credential.get_credential().is_err(),
|
||||
"Platform credential shouldn't exist yet!"
|
||||
);
|
||||
entry
|
||||
.set_password("test get_credential")
|
||||
.expect("Can't set password for get_credential");
|
||||
assert!(credential.get_credential().is_ok());
|
||||
entry
|
||||
.delete_credential()
|
||||
.expect("Couldn't delete after get_credential");
|
||||
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_update_attributes() {
|
||||
crate::tests::test_noop_get_update_attributes(entry_new);
|
||||
}
|
||||
}
|
||||
|
|
@ -189,10 +189,6 @@ pub mod secret_service;
|
|||
#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "apple-native"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))]
|
||||
pub mod ios;
|
||||
|
||||
//
|
||||
// pick the Windows keystore
|
||||
//
|
||||
|
|
@ -313,9 +309,9 @@ impl Entry {
|
|||
/// that matches this entry. This can only happen
|
||||
/// on some platforms, and then only if a third-party
|
||||
/// application wrote the ambiguous credential.
|
||||
pub fn set_password(&self, password: &str) -> Result<()> {
|
||||
pub async fn set_password(&self, password: &str) -> Result<()> {
|
||||
debug!("set password for entry {:?}", self.inner);
|
||||
self.inner.set_password(password)
|
||||
self.inner.set_password(password).await
|
||||
}
|
||||
|
||||
/// Set the secret for this entry.
|
||||
|
|
@ -325,9 +321,9 @@ impl Entry {
|
|||
/// that matches this entry. This can only happen
|
||||
/// on some platforms, and then only if a third-party
|
||||
/// application wrote the ambiguous credential.
|
||||
pub fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
pub async fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
debug!("set secret for entry {:?}", self.inner);
|
||||
self.inner.set_secret(secret)
|
||||
self.inner.set_secret(secret).await
|
||||
}
|
||||
|
||||
/// Retrieve the password saved for this entry.
|
||||
|
|
@ -339,9 +335,9 @@ impl Entry {
|
|||
/// that matches this entry. This can only happen
|
||||
/// on some platforms, and then only if a third-party
|
||||
/// application wrote the ambiguous credential.
|
||||
pub fn get_password(&self) -> Result<String> {
|
||||
pub async fn get_password(&self) -> Result<String> {
|
||||
debug!("get password from entry {:?}", self.inner);
|
||||
self.inner.get_password()
|
||||
self.inner.get_password().await
|
||||
}
|
||||
|
||||
/// Retrieve the secret saved for this entry.
|
||||
|
|
@ -353,9 +349,9 @@ impl Entry {
|
|||
/// that matches this entry. This can only happen
|
||||
/// on some platforms, and then only if a third-party
|
||||
/// application wrote the ambiguous credential.
|
||||
pub fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
pub async fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
debug!("get secret from entry {:?}", self.inner);
|
||||
self.inner.get_secret()
|
||||
self.inner.get_secret().await
|
||||
}
|
||||
|
||||
/// Get the attributes on the underlying credential for this entry.
|
||||
|
|
@ -371,9 +367,9 @@ impl Entry {
|
|||
/// that matches this entry. This can only happen
|
||||
/// on some platforms, and then only if a third-party
|
||||
/// application wrote the ambiguous credential.
|
||||
pub fn get_attributes(&self) -> Result<HashMap<String, String>> {
|
||||
pub async fn get_attributes(&self) -> Result<HashMap<String, String>> {
|
||||
debug!("get attributes from entry {:?}", self.inner);
|
||||
self.inner.get_attributes()
|
||||
self.inner.get_attributes().await
|
||||
}
|
||||
|
||||
/// Update the attributes on the underlying credential for this entry.
|
||||
|
|
@ -391,12 +387,12 @@ impl Entry {
|
|||
/// that matches this entry. This can only happen
|
||||
/// on some platforms, and then only if a third-party
|
||||
/// application wrote the ambiguous credential.
|
||||
pub fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
|
||||
pub async fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
|
||||
debug!(
|
||||
"update attributes for entry {:?} from map {attributes:?}",
|
||||
self.inner
|
||||
);
|
||||
self.inner.update_attributes(attributes)
|
||||
self.inner.update_attributes(attributes).await
|
||||
}
|
||||
|
||||
/// Delete the underlying credential for this entry.
|
||||
|
|
@ -412,9 +408,9 @@ impl Entry {
|
|||
/// Note: This does _not_ affect the lifetime of the [Entry]
|
||||
/// structure, which is controlled by Rust. It only
|
||||
/// affects the underlying credential store.
|
||||
pub fn delete_credential(&self) -> Result<()> {
|
||||
pub async fn delete_credential(&self) -> Result<()> {
|
||||
debug!("delete entry {:?}", self.inner);
|
||||
self.inner.delete_credential()
|
||||
self.inner.delete_credential().await
|
||||
}
|
||||
|
||||
/// Return a reference to this entry's wrapped credential.
|
||||
|
|
@ -474,12 +470,14 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
|
||||
async fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
|
||||
entry
|
||||
.set_password(in_pass)
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't set password for {case}: {err:?}"));
|
||||
let out_pass = entry
|
||||
.get_password()
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't get password for {case}: {err:?}"));
|
||||
assert_eq!(
|
||||
in_pass, out_pass,
|
||||
|
|
@ -488,12 +486,13 @@ mod tests {
|
|||
}
|
||||
|
||||
/// A basic round-trip unit test given an entry and a password.
|
||||
pub(crate) fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
|
||||
test_round_trip_no_delete(case, entry, in_pass);
|
||||
pub(crate) async fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
|
||||
test_round_trip_no_delete(case, entry, in_pass).await;
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
|
||||
let password = entry.get_password();
|
||||
let password = entry.get_password().await;
|
||||
assert!(
|
||||
matches!(password, Err(Error::NoEntry)),
|
||||
"Read deleted password for {case}",
|
||||
|
|
@ -501,12 +500,14 @@ mod tests {
|
|||
}
|
||||
|
||||
/// A basic round-trip unit test given an entry and a password.
|
||||
pub(crate) fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
|
||||
pub(crate) async fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
|
||||
entry
|
||||
.set_secret(in_secret)
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
|
||||
let out_secret = entry
|
||||
.get_secret()
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
|
||||
assert_eq!(
|
||||
in_secret, &out_secret,
|
||||
|
|
@ -514,8 +515,9 @@ mod tests {
|
|||
);
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
|
||||
let password = entry.get_secret();
|
||||
let password = entry.get_secret().await;
|
||||
assert!(
|
||||
matches!(password, Err(Error::NoEntry)),
|
||||
"Read deleted password for {case}",
|
||||
|
|
@ -551,101 +553,104 @@ mod tests {
|
|||
let name = generate_random_string();
|
||||
let entry = f(&name, &name);
|
||||
assert!(
|
||||
matches!(entry.get_password(), Err(Error::NoEntry)),
|
||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||
"Missing entry has password"
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn test_empty_password<F>(f: F)
|
||||
pub(crate) async fn test_empty_password<F>(f: F)
|
||||
where
|
||||
F: FnOnce(&str, &str) -> Entry,
|
||||
{
|
||||
let name = generate_random_string();
|
||||
let entry = f(&name, &name);
|
||||
test_round_trip("empty password", &entry, "");
|
||||
test_round_trip("empty password", &entry, "").await;
|
||||
}
|
||||
|
||||
pub(crate) fn test_round_trip_ascii_password<F>(f: F)
|
||||
pub(crate) async fn test_round_trip_ascii_password<F>(f: F)
|
||||
where
|
||||
F: FnOnce(&str, &str) -> Entry,
|
||||
{
|
||||
let name = generate_random_string();
|
||||
let entry = f(&name, &name);
|
||||
test_round_trip("ascii password", &entry, "test ascii password");
|
||||
test_round_trip("ascii password", &entry, "test ascii password").await;
|
||||
}
|
||||
|
||||
pub(crate) fn test_round_trip_non_ascii_password<F>(f: F)
|
||||
pub(crate) async fn test_round_trip_non_ascii_password<F>(f: F)
|
||||
where
|
||||
F: FnOnce(&str, &str) -> Entry,
|
||||
{
|
||||
let name = generate_random_string();
|
||||
let entry = f(&name, &name);
|
||||
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
|
||||
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です").await;
|
||||
}
|
||||
|
||||
pub(crate) fn test_round_trip_random_secret<F>(f: F)
|
||||
pub(crate) async fn test_round_trip_random_secret<F>(f: F)
|
||||
where
|
||||
F: FnOnce(&str, &str) -> Entry,
|
||||
{
|
||||
let name = generate_random_string();
|
||||
let entry = f(&name, &name);
|
||||
let secret = generate_random_bytes_of_len(24);
|
||||
test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
|
||||
test_round_trip_secret("non-ascii password", &entry, secret.as_slice()).await;
|
||||
}
|
||||
|
||||
pub(crate) fn test_update<F>(f: F)
|
||||
pub(crate) async fn test_update<F>(f: F)
|
||||
where
|
||||
F: FnOnce(&str, &str) -> Entry,
|
||||
{
|
||||
let name = generate_random_string();
|
||||
let entry = f(&name, &name);
|
||||
test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
|
||||
test_round_trip_no_delete("initial ascii password", &entry, "test ascii password").await;
|
||||
test_round_trip(
|
||||
"updated non-ascii password",
|
||||
&entry,
|
||||
"このきれいな花は桜です",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) fn test_noop_get_update_attributes<F>(f: F)
|
||||
pub(crate) async fn test_noop_get_update_attributes<F>(f: F)
|
||||
where
|
||||
F: FnOnce(&str, &str) -> Entry,
|
||||
{
|
||||
let name = generate_random_string();
|
||||
let entry = f(&name, &name);
|
||||
assert!(
|
||||
matches!(entry.get_attributes(), Err(Error::NoEntry)),
|
||||
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
|
||||
"Read missing credential in attribute test",
|
||||
);
|
||||
let mut map: HashMap<&str, &str> = HashMap::new();
|
||||
map.insert("test attribute name", "test attribute value");
|
||||
assert!(
|
||||
matches!(entry.update_attributes(&map), Err(Error::NoEntry)),
|
||||
matches!(entry.update_attributes(&map).await, Err(Error::NoEntry)),
|
||||
"Updated missing credential in attribute test",
|
||||
);
|
||||
// create the credential and test again
|
||||
entry
|
||||
.set_password("test password for attributes")
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
|
||||
match entry.get_attributes() {
|
||||
match entry.get_attributes().await {
|
||||
Err(err) => panic!("Couldn't get attributes: {err:?}"),
|
||||
Ok(attrs) if attrs.is_empty() => {}
|
||||
Ok(attrs) => panic!("Unexpected attributes: {attrs:?}"),
|
||||
}
|
||||
assert!(
|
||||
matches!(entry.update_attributes(&map), Ok(())),
|
||||
matches!(entry.update_attributes(&map).await, Ok(())),
|
||||
"Couldn't update attributes in attribute test",
|
||||
);
|
||||
match entry.get_attributes() {
|
||||
match entry.get_attributes().await {
|
||||
Err(err) => panic!("Couldn't get attributes after update: {err:?}"),
|
||||
Ok(attrs) if attrs.is_empty() => {}
|
||||
Ok(attrs) => panic!("Unexpected attributes after update: {attrs:?}"),
|
||||
}
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
|
||||
assert!(
|
||||
matches!(entry.get_attributes(), Err(Error::NoEntry)),
|
||||
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
|
||||
"Read deleted credential in attribute test",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ ignores all the others. so clients can't use it to access or update any attribut
|
|||
*/
|
||||
use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
|
||||
use crate::error::{Error as ErrorCode, Result, decode_password};
|
||||
use crate::ios::IosCredential;
|
||||
use security_framework::base::Error;
|
||||
use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain};
|
||||
use security_framework::os::macos::passwords::find_generic_password;
|
||||
|
|
@ -53,13 +52,14 @@ pub struct MacCredential {
|
|||
pub account: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CredentialApi for MacCredential {
|
||||
/// Create and write a credential with password for this entry.
|
||||
///
|
||||
/// The new credential replaces any existing one in the store.
|
||||
/// Since there is only one credential with a given _account_ and _user_
|
||||
/// in any given keychain, there is no chance of ambiguity.
|
||||
fn set_password(&self, password: &str) -> Result<()> {
|
||||
async fn set_password(&self, password: &str) -> Result<()> {
|
||||
get_keychain(self)?
|
||||
.set_generic_password(&self.service, &self.account, password.as_bytes())
|
||||
.map_err(decode_error)?;
|
||||
|
|
@ -71,7 +71,7 @@ impl CredentialApi for MacCredential {
|
|||
/// The new credential replaces any existing one in the store.
|
||||
/// Since there is only one credential with a given _account_ and _user_
|
||||
/// in any given keychain, there is no chance of ambiguity.
|
||||
fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
get_keychain(self)?
|
||||
.set_generic_password(&self.service, &self.account, secret)
|
||||
.map_err(decode_error)?;
|
||||
|
|
@ -82,7 +82,7 @@ impl CredentialApi for MacCredential {
|
|||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn get_password(&self) -> Result<String> {
|
||||
async fn get_password(&self) -> Result<String> {
|
||||
let (password_bytes, _) =
|
||||
find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account)
|
||||
.map_err(decode_error)?;
|
||||
|
|
@ -93,7 +93,7 @@ impl CredentialApi for MacCredential {
|
|||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
async fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
let (password_bytes, _) =
|
||||
find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account)
|
||||
.map_err(decode_error)?;
|
||||
|
|
@ -104,7 +104,7 @@ impl CredentialApi for MacCredential {
|
|||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn delete_credential(&self) -> Result<()> {
|
||||
async fn delete_credential(&self) -> Result<()> {
|
||||
let (_, item) =
|
||||
find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account)
|
||||
.map_err(decode_error)?;
|
||||
|
|
@ -199,16 +199,11 @@ impl CredentialBuilderApi for MacCredentialBuilder {
|
|||
} else {
|
||||
MacKeychainDomain::User
|
||||
};
|
||||
match domain {
|
||||
MacKeychainDomain::Protected => Ok(Box::new(IosCredential::new_with_target(
|
||||
None, service, user,
|
||||
)?)),
|
||||
_ => Ok(Box::new(MacCredential::new_with_target(
|
||||
Some(domain),
|
||||
service,
|
||||
user,
|
||||
)?)),
|
||||
}
|
||||
Ok(Box::new(MacCredential::new_with_target(
|
||||
Some(domain),
|
||||
service,
|
||||
user,
|
||||
)?))
|
||||
}
|
||||
|
||||
/// Return the underlying builder object with an `Any` type so that it can
|
||||
|
|
@ -331,38 +326,38 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_entry() {
|
||||
crate::tests::test_missing_entry(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_missing_entry() {
|
||||
crate::tests::test_missing_entry(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
crate::tests::test_update(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_update() {
|
||||
crate::tests::test_update(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_credential() {
|
||||
#[tokio::test]
|
||||
async fn test_get_credential() {
|
||||
let name = generate_random_string();
|
||||
let entry = entry_new(&name, &name);
|
||||
let credential: &MacCredential = entry
|
||||
|
|
@ -375,17 +370,19 @@ mod tests {
|
|||
);
|
||||
entry
|
||||
.set_password("test get_credential")
|
||||
.await
|
||||
.expect("Can't set password for get_credential");
|
||||
assert!(credential.get_credential().is_ok());
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("Couldn't delete after get_credential");
|
||||
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
|
||||
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_update_attributes() {
|
||||
crate::tests::test_noop_get_update_attributes(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_get_update_attributes() {
|
||||
crate::tests::test_noop_get_update_attributes(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -405,14 +402,5 @@ mod tests {
|
|||
)
|
||||
}
|
||||
}
|
||||
for name in ["data protection", "protected"] {
|
||||
let cred = Entry::new_with_target(name, name, name)
|
||||
.expect("couldn't create credential")
|
||||
.inner;
|
||||
let _: &super::IosCredential = cred
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.expect("credential not an iOS credential");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,13 +71,14 @@ pub struct MockData {
|
|||
pub error: Option<Error>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CredentialApi for MockCredential {
|
||||
/// Set a password on a mock credential.
|
||||
///
|
||||
/// If there is an error in the mock, it will be returned
|
||||
/// and the password will _not_ be set. The error will
|
||||
/// be cleared, so calling again will set the password.
|
||||
fn set_password(&self, password: &str) -> Result<()> {
|
||||
async fn set_password(&self, password: &str) -> Result<()> {
|
||||
let mut inner = self.inner.lock().expect("Can't access mock data for set");
|
||||
let data = inner.get_mut();
|
||||
let err = data.error.take();
|
||||
|
|
@ -95,7 +96,7 @@ impl CredentialApi for MockCredential {
|
|||
/// If there is an error in the mock, it will be returned
|
||||
/// and the password will _not_ be set. The error will
|
||||
/// be cleared, so calling again will set the password.
|
||||
fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
let mut inner = self.inner.lock().expect("Can't access mock data for set");
|
||||
let data = inner.get_mut();
|
||||
let err = data.error.take();
|
||||
|
|
@ -112,7 +113,7 @@ impl CredentialApi for MockCredential {
|
|||
///
|
||||
/// If there is an error set in the mock, it will
|
||||
/// be returned instead of a password.
|
||||
fn get_password(&self) -> Result<String> {
|
||||
async fn get_password(&self) -> Result<String> {
|
||||
let mut inner = self.inner.lock().expect("Can't access mock data for get");
|
||||
let data = inner.get_mut();
|
||||
let err = data.error.take();
|
||||
|
|
@ -129,7 +130,7 @@ impl CredentialApi for MockCredential {
|
|||
///
|
||||
/// If there is an error set in the mock, it will
|
||||
/// be returned instead of a password.
|
||||
fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
async fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
let mut inner = self.inner.lock().expect("Can't access mock data for get");
|
||||
let data = inner.get_mut();
|
||||
let err = data.error.take();
|
||||
|
|
@ -149,7 +150,7 @@ impl CredentialApi for MockCredential {
|
|||
///
|
||||
/// If there is no password, a [NoEntry](Error::NoEntry) error
|
||||
/// will be returned.
|
||||
fn delete_credential(&self) -> Result<()> {
|
||||
async fn delete_credential(&self) -> Result<()> {
|
||||
let mut inner = self
|
||||
.inner
|
||||
.lock()
|
||||
|
|
@ -253,43 +254,43 @@ mod tests {
|
|||
Entry::new_with_credential(Box::new(credential))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_entry() {
|
||||
crate::tests::test_missing_entry(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_missing_entry() {
|
||||
crate::tests::test_missing_entry(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
crate::tests::test_update(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_update() {
|
||||
crate::tests::test_update(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_update_attributes() {
|
||||
crate::tests::test_noop_get_update_attributes(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_get_update_attributes() {
|
||||
crate::tests::test_noop_get_update_attributes(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_error() {
|
||||
#[tokio::test]
|
||||
async fn test_set_error() {
|
||||
let name = generate_random_string();
|
||||
let entry = entry_new(&name, &name);
|
||||
let password = "test ascii password";
|
||||
|
|
@ -303,32 +304,37 @@ mod tests {
|
|||
"is an error".to_string(),
|
||||
));
|
||||
assert!(
|
||||
matches!(entry.set_password(password), Err(Error::Invalid(_, _))),
|
||||
matches!(
|
||||
entry.set_password(password).await,
|
||||
Err(Error::Invalid(_, _))
|
||||
),
|
||||
"set: No error"
|
||||
);
|
||||
entry
|
||||
.set_password(password)
|
||||
.await
|
||||
.expect("set: Error not cleared");
|
||||
mock.set_error(Error::NoEntry);
|
||||
assert!(
|
||||
matches!(entry.get_password(), Err(Error::NoEntry)),
|
||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||
"get: No error"
|
||||
);
|
||||
let stored_password = entry.get_password().expect("get: Error not cleared");
|
||||
let stored_password = entry.get_password().await.expect("get: Error not cleared");
|
||||
assert_eq!(
|
||||
stored_password, password,
|
||||
"Retrieved and set ascii passwords don't match"
|
||||
);
|
||||
mock.set_error(Error::TooLong("mock".to_string(), 3));
|
||||
assert!(
|
||||
matches!(entry.delete_credential(), Err(Error::TooLong(_, 3))),
|
||||
matches!(entry.delete_credential().await, Err(Error::TooLong(_, 3))),
|
||||
"delete: No error"
|
||||
);
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("delete: Error not cleared");
|
||||
assert!(
|
||||
matches!(entry.get_password(), Err(Error::NoEntry)),
|
||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||
"Able to read a deleted ascii password"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ issue for more details and possible workarounds.
|
|||
*/
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dbus_secret_service::{Collection, EncryptionType, Error, Item, SecretService};
|
||||
use secret_service::{Collection, EncryptionType, Error, Item, SecretService};
|
||||
|
||||
use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
|
||||
use crate::error::{Error as ErrorCode, Result, decode_password};
|
||||
|
|
@ -100,31 +100,38 @@ pub struct SsCredential {
|
|||
target: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CredentialApi for SsCredential {
|
||||
/// Sets the password on a unique matching item, if it exists, or creates one if necessary.
|
||||
///
|
||||
/// If there are multiple matches,
|
||||
/// returns an [Ambiguous](ErrorCode::Ambiguous) error with a credential for each
|
||||
/// matching item.
|
||||
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous) error with
|
||||
/// a credential for each matching item.
|
||||
///
|
||||
/// When creating, the item is put into a collection named by the credential's `target`
|
||||
/// attribute.
|
||||
fn set_password(&self, password: &str) -> Result<()> {
|
||||
self.set_secret(password.as_bytes())
|
||||
async fn set_password(&self, password: &str) -> Result<()> {
|
||||
self.set_secret(password.as_bytes()).await
|
||||
}
|
||||
|
||||
/// Sets the secret on a unique matching item, if it exists, or creates one if necessary.
|
||||
///
|
||||
/// If there are multiple matches,
|
||||
/// returns an [Ambiguous](ErrorCode::Ambiguous) error with a credential for each
|
||||
/// matching item.
|
||||
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous) error
|
||||
/// with a credential for each matching item.
|
||||
///
|
||||
/// When creating, the item is put into a collection named by the credential's `target`
|
||||
/// attribute.
|
||||
fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
// first try to find a unique, existing, matching item and set its password
|
||||
match self.map_matching_items(|i| set_item_secret(i, secret), true) {
|
||||
Ok(_) => return Ok(()),
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.matching_items(&ss).await {
|
||||
Ok(items) => {
|
||||
for item in items.iter() {
|
||||
set_item_secret(item, secret).await?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(ErrorCode::NoEntry) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
|
@ -132,9 +139,14 @@ impl CredentialApi for SsCredential {
|
|||
// an item, the credential must have an explicit target. All entries created with
|
||||
// the [new] or [new_with_target] commands will have explicit targets. But entries
|
||||
// created to wrap 3rd-party items that don't have `target` attributes may not.
|
||||
let ss = SecretService::connect(EncryptionType::Dh).map_err(platform_failure)?;
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
let name = self.target.as_ref().ok_or_else(empty_target)?;
|
||||
let collection = get_collection(&ss, name).or_else(|_| create_collection(&ss, name))?;
|
||||
let collection = match get_collection(&ss, name).await {
|
||||
Ok(collection) => collection,
|
||||
Err(_) => create_collection(&ss, name).await?,
|
||||
};
|
||||
collection
|
||||
.create_item(
|
||||
self.label.as_str(),
|
||||
|
|
@ -143,19 +155,29 @@ impl CredentialApi for SsCredential {
|
|||
true, // replace
|
||||
"text/plain",
|
||||
)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the password on a unique matching item, if it exists.
|
||||
///
|
||||
/// If there are no
|
||||
/// matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
|
||||
/// If there are multiple matches,
|
||||
/// returns an [Ambiguous](ErrorCode::Ambiguous)
|
||||
/// If there are no matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
|
||||
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous)
|
||||
/// error with a credential for each matching item.
|
||||
fn get_password(&self) -> Result<String> {
|
||||
Ok(self.map_matching_items(get_item_password, true)?.remove(0))
|
||||
async fn get_password(&self) -> Result<String> {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.unique_matching_item(&ss).await {
|
||||
Ok(item) => get_item_password(&item).await,
|
||||
Err(ErrorCode::NoEntry) => {
|
||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||
let item = self.find_unique_legacy_item(&collection).await?;
|
||||
get_item_password(&item).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the secret on a unique matching item, if it exists.
|
||||
|
|
@ -165,33 +187,71 @@ impl CredentialApi for SsCredential {
|
|||
/// If there are multiple matches,
|
||||
/// returns an [Ambiguous](ErrorCode::Ambiguous)
|
||||
/// error with a credential for each matching item.
|
||||
fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
Ok(self.map_matching_items(get_item_secret, true)?.remove(0))
|
||||
async fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.unique_matching_item(&ss).await {
|
||||
Ok(item) => get_item_secret(&item).await,
|
||||
Err(ErrorCode::NoEntry) => {
|
||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||
let item = self.find_unique_legacy_item(&collection).await?;
|
||||
get_item_secret(&item).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get attributes on a unique matching item, if it exists
|
||||
fn get_attributes(&self) -> Result<HashMap<String, String>> {
|
||||
let attributes: Vec<HashMap<String, String>> =
|
||||
self.map_matching_items(get_item_attributes, true)?;
|
||||
Ok(attributes.into_iter().next().unwrap())
|
||||
async fn get_attributes(&self) -> Result<HashMap<String, String>> {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.unique_matching_item(&ss).await {
|
||||
Ok(item) => get_item_attributes(&item).await,
|
||||
Err(ErrorCode::NoEntry) => {
|
||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||
let item = self.find_unique_legacy_item(&collection).await?;
|
||||
get_item_attributes(&item).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update attributes on a unique matching item, if it exists
|
||||
fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
|
||||
self.map_matching_items(|i| update_item_attributes(i, attributes), true)?;
|
||||
Ok(())
|
||||
async fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.unique_matching_item(&ss).await {
|
||||
Ok(item) => update_item_attributes(&item, attributes).await,
|
||||
Err(ErrorCode::NoEntry) => {
|
||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||
let item = self.find_unique_legacy_item(&collection).await?;
|
||||
update_item_attributes(&item, attributes).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the unique matching item, if it exists.
|
||||
///
|
||||
/// If there are no
|
||||
/// matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
|
||||
/// If there are multiple matches,
|
||||
/// returns an [Ambiguous](ErrorCode::Ambiguous)
|
||||
/// If there are no matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
|
||||
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous)
|
||||
/// error with a credential for each matching item.
|
||||
fn delete_credential(&self) -> Result<()> {
|
||||
self.map_matching_items(delete_item, true)?;
|
||||
Ok(())
|
||||
async fn delete_credential(&self) -> Result<()> {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.unique_matching_item(&ss).await {
|
||||
Ok(item) => delete_item(&item).await,
|
||||
Err(ErrorCode::NoEntry) => {
|
||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||
let item = self.find_unique_legacy_item(&collection).await?;
|
||||
delete_item(&item).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the underlying credential object with an `Any` type so that it can
|
||||
|
|
@ -230,7 +290,7 @@ impl SsCredential {
|
|||
Ok(Self {
|
||||
attributes,
|
||||
label: format!(
|
||||
"{user}@{service}:{target} (keyring v{})",
|
||||
"{user}@{service}:{target} (uv v{})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
target: Some(target.to_string()),
|
||||
|
|
@ -250,7 +310,7 @@ impl SsCredential {
|
|||
Ok(Self {
|
||||
attributes,
|
||||
label: format!(
|
||||
"keyring-rs v{} for no target, service '{service}', user '{user}'",
|
||||
"uv v{} for no target, service '{service}', user '{user}'",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
target: None,
|
||||
|
|
@ -261,90 +321,137 @@ impl SsCredential {
|
|||
///
|
||||
/// The created credential will have all the attributes and label
|
||||
/// of the underlying item, so you can examine them.
|
||||
pub fn new_from_item(item: &Item) -> Result<Self> {
|
||||
let attributes = item.get_attributes().map_err(decode_error)?;
|
||||
pub async fn new_from_item(item: &Item<'_>) -> Result<Self> {
|
||||
let attributes = item.get_attributes().await.map_err(decode_error)?;
|
||||
let target = attributes.get("target").cloned();
|
||||
Ok(Self {
|
||||
attributes,
|
||||
label: item.get_label().map_err(decode_error)?,
|
||||
label: item.get_label().await.map_err(decode_error)?,
|
||||
target,
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a credential for this credential's underlying matching item,
|
||||
/// if there is exactly one.
|
||||
pub fn new_from_matching_item(&self) -> Result<Self> {
|
||||
Ok(self
|
||||
.map_matching_items(Self::new_from_item, true)?
|
||||
.remove(0))
|
||||
pub async fn new_from_matching_item(&self) -> Result<Self> {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.unique_matching_item(&ss).await {
|
||||
Ok(item) => Self::new_from_item(&item).await,
|
||||
Err(ErrorCode::NoEntry) => {
|
||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||
let item = self.find_unique_legacy_item(&collection).await?;
|
||||
Self::new_from_item(&item).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// If there are multiple matching items for this credential, get all of their passwords.
|
||||
///
|
||||
/// (This is useful if [get_password](SsCredential::get_password)
|
||||
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
|
||||
pub fn get_all_passwords(&self) -> Result<Vec<String>> {
|
||||
self.map_matching_items(get_item_password, false)
|
||||
pub async fn get_all_passwords(&self) -> Result<Vec<String>> {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.matching_items(&ss).await {
|
||||
Ok(items) => {
|
||||
let mut passwords = Vec::new();
|
||||
for item in items.iter() {
|
||||
passwords.push(get_item_password(item).await?);
|
||||
}
|
||||
Ok(passwords)
|
||||
}
|
||||
Err(ErrorCode::NoEntry) => {
|
||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||
let items = self.find_legacy_items(&collection).await?;
|
||||
let mut passwords = Vec::new();
|
||||
for item in items.iter() {
|
||||
passwords.push(get_item_password(item).await?);
|
||||
}
|
||||
Ok(passwords)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// If there are multiple matching items for this credential, delete all of them.
|
||||
///
|
||||
/// (This is useful if [delete_credential](SsCredential::delete_credential)
|
||||
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
|
||||
pub fn delete_all_passwords(&self) -> Result<()> {
|
||||
self.map_matching_items(delete_item, false)?;
|
||||
Ok(())
|
||||
pub async fn delete_all_passwords(&self) -> Result<()> {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.map_err(platform_failure)?;
|
||||
match self.matching_items(&ss).await {
|
||||
Ok(items) => {
|
||||
for item in items.iter() {
|
||||
delete_item(item).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(ErrorCode::NoEntry) => {
|
||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||
let items = self.find_legacy_items(&collection).await?;
|
||||
for item in items.iter() {
|
||||
delete_item(item).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a function over the items matching this credential.
|
||||
/// Find and unlock items matching this credential.
|
||||
///
|
||||
/// Items are unlocked before the function is applied.
|
||||
///
|
||||
/// If `require_unique` is true, and there are no matching items, then
|
||||
/// a [NoEntry](ErrorCode::NoEntry) error is returned.
|
||||
/// If `require_unique` is true, and there are multiple matches,
|
||||
/// then an [Ambiguous](ErrorCode::Ambiguous) error is returned
|
||||
/// with a vector containing one
|
||||
/// credential for each of the matching items.
|
||||
pub fn map_matching_items<F, T>(&self, f: F, require_unique: bool) -> Result<Vec<T>>
|
||||
where
|
||||
F: Fn(&Item) -> Result<T>,
|
||||
T: Sized,
|
||||
{
|
||||
let ss = SecretService::connect(EncryptionType::Dh).map_err(platform_failure)?;
|
||||
/// Items are unlocked before being returned.
|
||||
async fn matching_items<'a>(&self, ss: &'a SecretService<'_>) -> Result<Vec<Item<'a>>> {
|
||||
let attributes: HashMap<&str, &str> = self.search_attributes(false).into_iter().collect();
|
||||
let search = ss.search_items(attributes).map_err(decode_error)?;
|
||||
let search = ss.search_items(attributes).await.map_err(decode_error)?;
|
||||
let count = search.locked.len() + search.unlocked.len();
|
||||
if count == 0 {
|
||||
if let Some("default") = self.target.as_deref() {
|
||||
return self.map_matching_legacy_items(&ss, f, require_unique);
|
||||
}
|
||||
return Err(ErrorCode::NoEntry);
|
||||
}
|
||||
if require_unique {
|
||||
if count == 0 {
|
||||
return Err(ErrorCode::NoEntry);
|
||||
} else if count > 1 {
|
||||
|
||||
let mut results: Vec<Item<'a>> = vec![];
|
||||
for item in search.unlocked {
|
||||
results.push(item);
|
||||
}
|
||||
for item in search.locked {
|
||||
item.unlock().await.map_err(decode_error)?;
|
||||
results.push(item);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Find and unlock a unique item matching this credential.
|
||||
///
|
||||
/// If there are no matching items, then a [NoEntry](ErrorCode::NoEntry) error is returned.
|
||||
/// If there are multiple matches, then an [Ambiguous](ErrorCode::Ambiguous) error is
|
||||
/// returned with a vector containing one credential for each of the matching items.
|
||||
async fn unique_matching_item<'a>(&self, ss: &'a SecretService<'_>) -> Result<Item<'a>> {
|
||||
let mut items = self.matching_items(ss).await?;
|
||||
match items.len() {
|
||||
0 => Err(ErrorCode::NoEntry),
|
||||
1 => Ok(items.pop().unwrap()),
|
||||
_ => {
|
||||
let mut creds: Vec<Box<Credential>> = vec![];
|
||||
let attributes: HashMap<&str, &str> =
|
||||
self.search_attributes(false).into_iter().collect();
|
||||
let search = ss.search_items(attributes).await.map_err(decode_error)?;
|
||||
for item in search.locked.iter().chain(search.unlocked.iter()) {
|
||||
let cred = Self::new_from_item(item)?;
|
||||
let cred = Self::new_from_item(item).await?;
|
||||
creds.push(Box::new(cred))
|
||||
}
|
||||
return Err(ErrorCode::Ambiguous(creds));
|
||||
}
|
||||
}
|
||||
let mut results: Vec<T> = vec![];
|
||||
for item in search.unlocked.iter() {
|
||||
results.push(f(item)?);
|
||||
}
|
||||
for item in search.locked.iter() {
|
||||
item.unlock().map_err(decode_error)?;
|
||||
results.push(f(item)?);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Map a function over items that older versions of keyring
|
||||
/// would have matched against this credential.
|
||||
/// Find legacy items in the given collection if the credential being searched has
|
||||
/// the default target.
|
||||
///
|
||||
/// Keyring v1 created secret service items that had no target attribute, and it was
|
||||
/// only able to create items in the default collection. Keyring v2, and Keyring v3.1,
|
||||
|
|
@ -362,40 +469,59 @@ impl SsCredential {
|
|||
/// collection.
|
||||
///
|
||||
/// So with keyring v3.2.1, if the service-wide search fails to find any matching
|
||||
/// credential, and the credential being searched for has the default target (or
|
||||
/// no target), we fall back and search the default collection for a v1-style credential.
|
||||
/// credential, and the credential being searched for has the default target,
|
||||
/// we fall back and search the default collection for a v1-style credential.
|
||||
/// That preserves the legacy behavior at the cost of a second round-trip through
|
||||
/// the secret service for the collection search.
|
||||
pub fn map_matching_legacy_items<F, T>(
|
||||
&self,
|
||||
ss: &SecretService,
|
||||
f: F,
|
||||
require_unique: bool,
|
||||
) -> Result<Vec<T>>
|
||||
where
|
||||
F: Fn(&Item) -> Result<T>,
|
||||
T: Sized,
|
||||
{
|
||||
let collection = ss.get_default_collection().map_err(decode_error)?;
|
||||
let attributes = self.search_attributes(true);
|
||||
let search = collection.search_items(attributes).map_err(decode_error)?;
|
||||
if require_unique {
|
||||
if search.is_empty() && require_unique {
|
||||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there are no matching items or
|
||||
/// if the credential being searched does not have the default target.
|
||||
async fn find_legacy_items<'a>(&self, collection: &'a Collection<'_>) -> Result<Vec<Item<'a>>> {
|
||||
if let Some("default") = self.target.as_deref() {
|
||||
let attributes = self.search_attributes(true);
|
||||
let search = collection
|
||||
.search_items(attributes)
|
||||
.await
|
||||
.map_err(decode_error)?;
|
||||
if search.is_empty() {
|
||||
return Err(ErrorCode::NoEntry);
|
||||
} else if search.len() > 1 {
|
||||
}
|
||||
|
||||
Ok(search)
|
||||
} else {
|
||||
Err(ErrorCode::NoEntry)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find unique legacy item in the given collection if the credential being searched has
|
||||
/// the default target.
|
||||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there are no matching items or
|
||||
/// if the credential being searched does not have the default target.
|
||||
/// If there are multiple matches, then an [Ambiguous](ErrorCode::Ambiguous) error is
|
||||
/// returned with a vector containing one credential for each of the matching items.
|
||||
async fn find_unique_legacy_item<'a>(
|
||||
&self,
|
||||
collection: &'a Collection<'_>,
|
||||
) -> Result<Item<'a>> {
|
||||
let mut items = self.find_legacy_items(collection).await?;
|
||||
match items.len() {
|
||||
0 => Err(ErrorCode::NoEntry),
|
||||
1 => Ok(items.pop().unwrap()),
|
||||
_ => {
|
||||
let mut creds: Vec<Box<Credential>> = vec![];
|
||||
let attributes = self.search_attributes(true);
|
||||
let search = collection
|
||||
.search_items(attributes)
|
||||
.await
|
||||
.map_err(decode_error)?;
|
||||
for item in search.iter() {
|
||||
let cred = Self::new_from_item(item)?;
|
||||
let cred = Self::new_from_item(item).await?;
|
||||
creds.push(Box::new(cred))
|
||||
}
|
||||
return Err(ErrorCode::Ambiguous(creds));
|
||||
}
|
||||
}
|
||||
let mut results: Vec<T> = vec![];
|
||||
for item in search.iter() {
|
||||
results.push(f(item)?);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Using strings in the credential map makes managing the lifetime
|
||||
|
|
@ -457,18 +583,23 @@ impl CredentialBuilderApi for SsCredentialBuilder {
|
|||
///
|
||||
/// The name `default` is treated specially and is interpreted as naming
|
||||
/// the default collection regardless of its label (which might be different).
|
||||
pub fn get_collection<'a>(ss: &'a SecretService, name: &str) -> Result<Collection<'a>> {
|
||||
pub async fn get_collection<'a>(ss: &'a SecretService<'_>, name: &str) -> Result<Collection<'a>> {
|
||||
let collection = if name.eq("default") {
|
||||
ss.get_default_collection().map_err(decode_error)?
|
||||
ss.get_default_collection().await.map_err(decode_error)?
|
||||
} else {
|
||||
let all = ss.get_all_collections().map_err(decode_error)?;
|
||||
let found = all
|
||||
.into_iter()
|
||||
.find(|c| c.get_label().map(|l| l.eq(name)).unwrap_or(false));
|
||||
let all = ss.get_all_collections().await.map_err(decode_error)?;
|
||||
let mut found = None;
|
||||
for c in all {
|
||||
let label = c.get_label().await.map_err(decode_error)?;
|
||||
if label.eq(name) {
|
||||
found = Some(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
found.ok_or(ErrorCode::NoEntry)?
|
||||
};
|
||||
if collection.is_locked().map_err(decode_error)? {
|
||||
collection.unlock().map_err(decode_error)?;
|
||||
if collection.is_locked().await.map_err(decode_error)? {
|
||||
collection.unlock().await.map_err(decode_error)?;
|
||||
}
|
||||
Ok(collection)
|
||||
}
|
||||
|
|
@ -478,45 +609,56 @@ pub fn get_collection<'a>(ss: &'a SecretService, name: &str) -> Result<Collectio
|
|||
/// If a collection with that name already exists, it is returned.
|
||||
///
|
||||
/// The name `default` is specially interpreted to mean the default collection.
|
||||
pub fn create_collection<'a>(ss: &'a SecretService, name: &str) -> Result<Collection<'a>> {
|
||||
pub async fn create_collection<'a>(
|
||||
ss: &'a SecretService<'_>,
|
||||
name: &str,
|
||||
) -> Result<Collection<'a>> {
|
||||
let collection = if name.eq("default") {
|
||||
ss.get_default_collection().map_err(decode_error)?
|
||||
ss.get_default_collection().await.map_err(decode_error)?
|
||||
} else {
|
||||
ss.create_collection(name, "").map_err(decode_error)?
|
||||
ss.create_collection(name, "").await.map_err(decode_error)?
|
||||
};
|
||||
Ok(collection)
|
||||
}
|
||||
|
||||
/// Given an existing item, set its secret.
|
||||
pub fn set_item_secret(item: &Item, secret: &[u8]) -> Result<()> {
|
||||
item.set_secret(secret, "text/plain").map_err(decode_error)
|
||||
pub async fn set_item_secret(item: &Item<'_>, secret: &[u8]) -> Result<()> {
|
||||
item.set_secret(secret, "text/plain")
|
||||
.await
|
||||
.map_err(decode_error)
|
||||
}
|
||||
|
||||
/// Given an existing item, retrieve and decode its password.
|
||||
pub fn get_item_password(item: &Item) -> Result<String> {
|
||||
let bytes = item.get_secret().map_err(decode_error)?;
|
||||
pub async fn get_item_password(item: &Item<'_>) -> Result<String> {
|
||||
let bytes = item.get_secret().await.map_err(decode_error)?;
|
||||
decode_password(bytes)
|
||||
}
|
||||
|
||||
/// Given an existing item, retrieve its secret.
|
||||
pub fn get_item_secret(item: &Item) -> Result<Vec<u8>> {
|
||||
let secret = item.get_secret().map_err(decode_error)?;
|
||||
pub async fn get_item_secret(item: &Item<'_>) -> Result<Vec<u8>> {
|
||||
let secret = item.get_secret().await.map_err(decode_error)?;
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
/// Given an existing item, retrieve its non-controlled attributes.
|
||||
pub fn get_item_attributes(item: &Item) -> Result<HashMap<String, String>> {
|
||||
let mut attributes = item.get_attributes().map_err(decode_error)?;
|
||||
pub async fn get_item_attributes(item: &Item<'_>) -> Result<HashMap<String, String>> {
|
||||
let mut attributes = item.get_attributes().await.map_err(decode_error)?;
|
||||
attributes.remove("target");
|
||||
attributes.remove("service");
|
||||
attributes.remove("username");
|
||||
attributes.insert("label".to_string(), item.get_label().map_err(decode_error)?);
|
||||
attributes.insert(
|
||||
"label".to_string(),
|
||||
item.get_label().await.map_err(decode_error)?,
|
||||
);
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Given an existing item, retrieve its non-controlled attributes.
|
||||
pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) -> Result<()> {
|
||||
let existing = item.get_attributes().map_err(decode_error)?;
|
||||
pub async fn update_item_attributes(
|
||||
item: &Item<'_>,
|
||||
attributes: &HashMap<&str, &str>,
|
||||
) -> Result<()> {
|
||||
let existing = item.get_attributes().await.map_err(decode_error)?;
|
||||
let mut updated: HashMap<&str, &str> = HashMap::new();
|
||||
for (k, v) in existing.iter() {
|
||||
updated.insert(k, v);
|
||||
|
|
@ -532,7 +674,7 @@ pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) ->
|
|||
"cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
item.set_label(v).map_err(decode_error)?;
|
||||
item.set_label(v).await.map_err(decode_error)?;
|
||||
if updated.contains_key("label") {
|
||||
updated.insert("label", v);
|
||||
}
|
||||
|
|
@ -540,13 +682,13 @@ pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) ->
|
|||
updated.insert(k, v);
|
||||
}
|
||||
}
|
||||
item.set_attributes(updated).map_err(decode_error)?;
|
||||
item.set_attributes(updated).await.map_err(decode_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Given an existing item, delete it.
|
||||
pub fn delete_item(item: &Item) -> Result<()> {
|
||||
item.delete().map_err(decode_error)
|
||||
pub async fn delete_item(item: &Item<'_>) -> Result<()> {
|
||||
item.delete().await.map_err(decode_error)
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -609,47 +751,43 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_service_and_user() {
|
||||
crate::tests::test_empty_service_and_user(entry_new);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_missing_entry() {
|
||||
crate::tests::test_missing_entry(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
crate::tests::test_update(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_update() {
|
||||
crate::tests::test_update(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_credential() {
|
||||
#[tokio::test]
|
||||
async fn test_get_credential() {
|
||||
let name = generate_random_string();
|
||||
let entry = entry_new(&name, &name);
|
||||
entry
|
||||
.set_password("test get credential")
|
||||
.await
|
||||
.expect("Can't set password for get_credential");
|
||||
let credential: &SsCredential = entry
|
||||
.get_credential()
|
||||
|
|
@ -657,6 +795,7 @@ mod tests {
|
|||
.expect("Not a secret service credential");
|
||||
let actual = credential
|
||||
.new_from_matching_item()
|
||||
.await
|
||||
.expect("Can't read credential");
|
||||
assert_eq!(actual.label, credential.label, "Labels don't match");
|
||||
for (key, value) in &credential.attributes {
|
||||
|
|
@ -668,19 +807,20 @@ mod tests {
|
|||
}
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("Couldn't delete get-credential");
|
||||
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
|
||||
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_update_attributes() {
|
||||
#[tokio::test]
|
||||
async fn test_get_update_attributes() {
|
||||
let name = generate_random_string();
|
||||
let credential = SsCredential::new_with_target(None, &name, &name)
|
||||
.expect("Can't create credential for attribute test");
|
||||
let create_label = credential.label.clone();
|
||||
let entry = Entry::new_with_credential(Box::new(credential));
|
||||
assert!(
|
||||
matches!(entry.get_attributes(), Err(Error::NoEntry)),
|
||||
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
|
||||
"Read missing credential in attribute test",
|
||||
);
|
||||
let mut in_map: HashMap<&str, &str> = HashMap::new();
|
||||
|
|
@ -690,15 +830,17 @@ mod tests {
|
|||
in_map.insert("service", "ignored service value");
|
||||
in_map.insert("username", "ignored username value");
|
||||
assert!(
|
||||
matches!(entry.update_attributes(&in_map), Err(Error::NoEntry)),
|
||||
matches!(entry.update_attributes(&in_map).await, Err(Error::NoEntry)),
|
||||
"Updated missing credential in attribute test",
|
||||
);
|
||||
// create the credential and test again
|
||||
entry
|
||||
.set_password("test password for attributes")
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
|
||||
let out_map = entry
|
||||
.get_attributes()
|
||||
.await
|
||||
.expect("Can't get attributes after create");
|
||||
assert_eq!(out_map["label"], create_label);
|
||||
assert_eq!(out_map["application"], "rust-keyring");
|
||||
|
|
@ -706,11 +848,12 @@ mod tests {
|
|||
assert!(!out_map.contains_key("service"));
|
||||
assert!(!out_map.contains_key("username"));
|
||||
assert!(
|
||||
matches!(entry.update_attributes(&in_map), Ok(())),
|
||||
matches!(entry.update_attributes(&in_map).await, Ok(())),
|
||||
"Couldn't update attributes in attribute test",
|
||||
);
|
||||
let after_map = entry
|
||||
.get_attributes()
|
||||
.await
|
||||
.expect("Can't get attributes after update");
|
||||
assert_eq!(after_map["label"], in_map["label"]);
|
||||
assert_eq!(
|
||||
|
|
@ -720,21 +863,25 @@ mod tests {
|
|||
assert_eq!(out_map["application"], "rust-keyring");
|
||||
in_map.insert("label", "");
|
||||
assert!(
|
||||
matches!(entry.update_attributes(&in_map), Err(Error::Invalid(_, _))),
|
||||
matches!(
|
||||
entry.update_attributes(&in_map).await,
|
||||
Err(Error::Invalid(_, _))
|
||||
),
|
||||
"Was able to set empty label in attribute test",
|
||||
);
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
|
||||
assert!(
|
||||
matches!(entry.get_attributes(), Err(Error::NoEntry)),
|
||||
matches!(entry.get_attributes().await, Err(Error::NoEntry)),
|
||||
"Read deleted credential in attribute test",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[ignore = "can't be run headless, because it needs to prompt"]
|
||||
fn test_create_new_target_collection() {
|
||||
async fn test_create_new_target_collection() {
|
||||
let name = generate_random_string();
|
||||
let credential = SsCredential::new_with_target(Some(&name), &name, &name)
|
||||
.expect("Can't create credential for new collection");
|
||||
|
|
@ -742,21 +889,24 @@ mod tests {
|
|||
let password = "password in new collection";
|
||||
entry
|
||||
.set_password(password)
|
||||
.await
|
||||
.expect("Can't set password for new collection entry");
|
||||
let actual = entry
|
||||
.get_password()
|
||||
.await
|
||||
.expect("Can't get password for new collection entry");
|
||||
assert_eq!(actual, password);
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("Couldn't delete password for new collection entry");
|
||||
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
|
||||
delete_collection(&name);
|
||||
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)));
|
||||
delete_collection(&name).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[ignore = "can't be run headless, because it needs to prompt"]
|
||||
fn test_separate_targets_dont_interfere() {
|
||||
async fn test_separate_targets_dont_interfere() {
|
||||
let name1 = generate_random_string();
|
||||
let name2 = generate_random_string();
|
||||
let credential1 = SsCredential::new_with_target(Some(&name1), &name1, &name1)
|
||||
|
|
@ -771,72 +921,90 @@ mod tests {
|
|||
let password3 = "password for default collection";
|
||||
entry1
|
||||
.set_password(password1)
|
||||
.await
|
||||
.expect("Can't set password for collection 1");
|
||||
entry2
|
||||
.set_password(password2)
|
||||
.await
|
||||
.expect("Can't set password for collection 2");
|
||||
entry3
|
||||
.set_password(password3)
|
||||
.await
|
||||
.expect("Can't set password for default collection");
|
||||
let actual1 = entry1
|
||||
.get_password()
|
||||
.await
|
||||
.expect("Can't get password for collection 1");
|
||||
assert_eq!(actual1, password1);
|
||||
let actual2 = entry2
|
||||
.get_password()
|
||||
.await
|
||||
.expect("Can't get password for collection 2");
|
||||
assert_eq!(actual2, password2);
|
||||
let actual3 = entry3
|
||||
.get_password()
|
||||
.await
|
||||
.expect("Can't get password for default collection");
|
||||
assert_eq!(actual3, password3);
|
||||
entry1
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("Couldn't delete password for collection 1");
|
||||
assert!(matches!(entry1.get_password(), Err(Error::NoEntry)));
|
||||
assert!(matches!(entry1.get_password().await, Err(Error::NoEntry)));
|
||||
entry2
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("Couldn't delete password for collection 2");
|
||||
assert!(matches!(entry2.get_password(), Err(Error::NoEntry)));
|
||||
assert!(matches!(entry2.get_password().await, Err(Error::NoEntry)));
|
||||
entry3
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("Couldn't delete password for default collection");
|
||||
assert!(matches!(entry3.get_password(), Err(Error::NoEntry)));
|
||||
delete_collection(&name1);
|
||||
delete_collection(&name2);
|
||||
assert!(matches!(entry3.get_password().await, Err(Error::NoEntry)));
|
||||
delete_collection(&name1).await;
|
||||
delete_collection(&name2).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_entry() {
|
||||
#[tokio::test]
|
||||
async fn test_legacy_entry() {
|
||||
let name = generate_random_string();
|
||||
let pw = "test password";
|
||||
let v3_entry = Entry::new(&name, &name).expect("Can't create v3 entry");
|
||||
let _ = v3_entry.get_password().expect_err("Found v3 entry");
|
||||
create_v1_entry(&name, pw);
|
||||
let password = v3_entry.get_password().expect("Can't find v1 entry");
|
||||
let _ = v3_entry.get_password().await.expect_err("Found v3 entry");
|
||||
create_v1_entry(&name, pw).await;
|
||||
let password = v3_entry.get_password().await.expect("Can't find v1 entry");
|
||||
assert_eq!(password, pw);
|
||||
v3_entry.delete_credential().expect("Can't delete v1 entry");
|
||||
v3_entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("Can't delete v1 entry");
|
||||
let _ = v3_entry
|
||||
.get_password()
|
||||
.await
|
||||
.expect_err("Got password for v1 entry after delete");
|
||||
}
|
||||
|
||||
fn delete_collection(name: &str) {
|
||||
let ss =
|
||||
SecretService::connect(EncryptionType::Dh).expect("Can't connect to secret service");
|
||||
let collection = super::get_collection(&ss, name).expect("Can't find collection to delete");
|
||||
collection.delete().expect("Can't delete collection");
|
||||
async fn delete_collection(name: &str) {
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.expect("Can't connect to secret service");
|
||||
let collection = super::get_collection(&ss, name)
|
||||
.await
|
||||
.expect("Can't find collection to delete");
|
||||
collection.delete().await.expect("Can't delete collection");
|
||||
}
|
||||
|
||||
fn create_v1_entry(name: &str, password: &str) {
|
||||
use dbus_secret_service::{EncryptionType, SecretService};
|
||||
async fn create_v1_entry(name: &str, password: &str) {
|
||||
use secret_service::{EncryptionType, SecretService};
|
||||
|
||||
let cred = SsCredential::new_with_no_target(name, name)
|
||||
.expect("Can't create credential with no target");
|
||||
let ss =
|
||||
SecretService::connect(EncryptionType::Dh).expect("Can't connect to secret service");
|
||||
let ss = SecretService::connect(EncryptionType::Dh)
|
||||
.await
|
||||
.expect("Can't connect to secret service");
|
||||
let collection = ss
|
||||
.get_default_collection()
|
||||
.await
|
||||
.expect("Can't get default collection");
|
||||
collection
|
||||
.create_item(
|
||||
|
|
@ -846,6 +1014,7 @@ mod tests {
|
|||
true, // replace
|
||||
"text/plain",
|
||||
)
|
||||
.await
|
||||
.expect("Can't create item with no target in default collection");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ impl CredentialApi for WinCredential {
|
|||
/// The new credential replaces any existing one in the store.
|
||||
/// Since there is only one credential with a given _target name_,
|
||||
/// there is no chance of ambiguity.
|
||||
fn set_password(&self, password: &str) -> Result<()> {
|
||||
async fn set_password(&self, password: &str) -> Result<()> {
|
||||
self.validate_attributes(None, Some(password))?;
|
||||
// Password strings are converted to UTF-16, because that's the native
|
||||
// charset for Windows strings. This allows interoperability with native
|
||||
|
|
@ -98,7 +98,7 @@ impl CredentialApi for WinCredential {
|
|||
/// The new credential replaces any existing one in the store.
|
||||
/// Since there is only one credential with a given _target name_,
|
||||
/// there is no chance of ambiguity.
|
||||
fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
async fn set_secret(&self, secret: &[u8]) -> Result<()> {
|
||||
self.validate_attributes(Some(secret), None)?;
|
||||
self.save_credential(secret)
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ impl CredentialApi for WinCredential {
|
|||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn get_password(&self) -> Result<String> {
|
||||
async fn get_password(&self) -> Result<String> {
|
||||
self.extract_from_platform(extract_password)
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ impl CredentialApi for WinCredential {
|
|||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
async fn get_secret(&self) -> Result<Vec<u8>> {
|
||||
self.extract_from_platform(extract_secret)
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ impl CredentialApi for WinCredential {
|
|||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn get_attributes(&self) -> Result<HashMap<String, String>> {
|
||||
async fn get_attributes(&self) -> Result<HashMap<String, String>> {
|
||||
let cred = self.extract_from_platform(Self::extract_credential)?;
|
||||
let mut attributes: HashMap<String, String> = HashMap::new();
|
||||
attributes.insert("comment".to_string(), cred.comment.clone());
|
||||
|
|
@ -136,7 +136,7 @@ impl CredentialApi for WinCredential {
|
|||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
|
||||
async fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> {
|
||||
let secret = self.extract_from_platform(extract_secret)?;
|
||||
let mut cred = self.extract_from_platform(Self::extract_credential)?;
|
||||
if let Some(comment) = attributes.get(&"comment") {
|
||||
|
|
@ -156,7 +156,7 @@ impl CredentialApi for WinCredential {
|
|||
///
|
||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
||||
/// credential in the store.
|
||||
fn delete_credential(&self) -> Result<()> {
|
||||
async fn delete_credential(&self) -> Result<()> {
|
||||
self.validate_attributes(None, None)?;
|
||||
let target_name = to_wstr(&self.target_name);
|
||||
let cred_type = CRED_TYPE_GENERIC;
|
||||
|
|
@ -528,13 +528,14 @@ mod tests {
|
|||
assert!(matches!(
|
||||
default_credential_builder().persistence(),
|
||||
CredentialPersistence::UntilDelete
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
fn entry_new(service: &str, user: &str) -> Entry {
|
||||
crate::tests::entry_from_constructor(WinCredential::new_with_target, service, user)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
#[test]
|
||||
fn test_bad_password() {
|
||||
fn make_platform_credential(password: &mut Vec<u8>) -> CREDENTIALW {
|
||||
|
|
@ -592,7 +593,7 @@ mod tests {
|
|||
assert_eq!(val, len, "Error names wrong limit");
|
||||
}
|
||||
Err(other) => panic!("Error is not '{attr} too long': {other}"),
|
||||
Ok(_) => panic!("No error when {attr} too long"),
|
||||
Ok(()) => panic!("No error when {attr} too long"),
|
||||
}
|
||||
}
|
||||
let cred = WinCredential {
|
||||
|
|
@ -622,11 +623,11 @@ mod tests {
|
|||
match attr {
|
||||
"password" => {
|
||||
let password = generate_random_string_of_len((len / 2) as usize + 1);
|
||||
validate(bad_cred.validate_attributes(None, Some(&password)))
|
||||
validate(bad_cred.validate_attributes(None, Some(&password)));
|
||||
}
|
||||
"secret" => {
|
||||
let secret: Vec<u8> = vec![255u8; len as usize + 1];
|
||||
validate(bad_cred.validate_attributes(Some(&secret), None))
|
||||
validate(bad_cred.validate_attributes(Some(&secret), None));
|
||||
}
|
||||
_ => validate(bad_cred.validate_attributes(None, None)),
|
||||
}
|
||||
|
|
@ -659,49 +660,44 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_service_and_user() {
|
||||
crate::tests::test_empty_service_and_user(entry_new);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_missing_entry() {
|
||||
crate::tests::test_missing_entry(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_empty_password() {
|
||||
crate::tests::test_empty_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_ascii_password() {
|
||||
crate::tests::test_round_trip_ascii_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_non_ascii_password() {
|
||||
crate::tests::test_round_trip_non_ascii_password(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_random_secret() {
|
||||
crate::tests::test_round_trip_random_secret(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
crate::tests::test_update(entry_new);
|
||||
#[tokio::test]
|
||||
async fn test_update() {
|
||||
crate::tests::test_update(entry_new).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_update_attributes() {
|
||||
#[tokio::test]
|
||||
async fn test_get_update_attributes() {
|
||||
let name = generate_random_string();
|
||||
let cred = WinCredential::new_with_target(None, &name, &name)
|
||||
.expect("Can't create credential for attribute test");
|
||||
let entry = Entry::new_with_credential(Box::new(cred.clone()));
|
||||
assert!(
|
||||
matches!(entry.get_attributes(), Err(ErrorCode::NoEntry)),
|
||||
matches!(entry.get_attributes().await, Err(ErrorCode::NoEntry)),
|
||||
"Read missing credential in attribute test",
|
||||
);
|
||||
let mut in_map: HashMap<&str, &str> = HashMap::new();
|
||||
|
|
@ -711,25 +707,31 @@ mod tests {
|
|||
in_map.insert("comment", "comment value");
|
||||
in_map.insert("username", "username value");
|
||||
assert!(
|
||||
matches!(entry.update_attributes(&in_map), Err(ErrorCode::NoEntry)),
|
||||
matches!(
|
||||
entry.update_attributes(&in_map).await,
|
||||
Err(ErrorCode::NoEntry)
|
||||
),
|
||||
"Updated missing credential in attribute test",
|
||||
);
|
||||
// create the credential and test again
|
||||
entry
|
||||
.set_password("test password for attributes")
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}"));
|
||||
let out_map = entry
|
||||
.get_attributes()
|
||||
.await
|
||||
.expect("Can't get attributes after create");
|
||||
assert_eq!(out_map["target_alias"], cred.target_alias);
|
||||
assert_eq!(out_map["comment"], cred.comment);
|
||||
assert_eq!(out_map["username"], cred.username);
|
||||
assert!(
|
||||
matches!(entry.update_attributes(&in_map), Ok(())),
|
||||
matches!(entry.update_attributes(&in_map).await, Ok(())),
|
||||
"Couldn't update attributes in attribute test",
|
||||
);
|
||||
let after_map = entry
|
||||
.get_attributes()
|
||||
.await
|
||||
.expect("Can't get attributes after update");
|
||||
assert_eq!(after_map["target_alias"], in_map["target_alias"]);
|
||||
assert_eq!(after_map["comment"], in_map["comment"]);
|
||||
|
|
@ -738,20 +740,22 @@ mod tests {
|
|||
assert!(!after_map.contains_key("attribute name"));
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}"));
|
||||
assert!(
|
||||
matches!(entry.get_attributes(), Err(ErrorCode::NoEntry)),
|
||||
matches!(entry.get_attributes().await, Err(ErrorCode::NoEntry)),
|
||||
"Read deleted credential in attribute test",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_credential() {
|
||||
#[tokio::test]
|
||||
async fn test_get_credential() {
|
||||
let name = generate_random_string();
|
||||
let entry = entry_new(&name, &name);
|
||||
let password = "test get password";
|
||||
entry
|
||||
.set_password(password)
|
||||
.await
|
||||
.expect("Can't set test get password");
|
||||
let credential: &WinCredential = entry
|
||||
.get_credential()
|
||||
|
|
@ -773,7 +777,11 @@ mod tests {
|
|||
assert_eq!(actual.comment, credential.comment, "Comments don't match");
|
||||
entry
|
||||
.delete_credential()
|
||||
.await
|
||||
.expect("Couldn't delete get-credential");
|
||||
assert!(matches!(entry.get_password(), Err(ErrorCode::NoEntry)));
|
||||
assert!(matches!(
|
||||
entry.get_password().await,
|
||||
Err(ErrorCode::NoEntry)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue