mirror of https://github.com/astral-sh/uv
Update style, remove ios code paths, and update docs
This commit is contained in:
parent
bda4e7bb4f
commit
dab39836b1
|
|
@ -3540,37 +3540,6 @@ version = "0.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rpassword"
|
|
||||||
version = "7.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"rtoolbox",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rprompt"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "69abf524bb9ccb7c071f7231441288d74b48d176cb309eb00e6f77d186c6e035"
|
|
||||||
dependencies = [
|
|
||||||
"rtoolbox",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtoolbox"
|
|
||||||
version = "0.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-netrc"
|
name = "rust-netrc"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
@ -5790,19 +5759,13 @@ name = "uv-keyring"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"clap",
|
|
||||||
"doc-comment",
|
"doc-comment",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"log",
|
|
||||||
"rpassword",
|
|
||||||
"rprompt",
|
|
||||||
"secret-service",
|
"secret-service",
|
||||||
"security-framework",
|
"security-framework",
|
||||||
"tokio",
|
"tokio",
|
||||||
"whoami",
|
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["apple-native", "secret-service", "windows-native"]
|
default = ["apple-native", "secret-service", "windows-native"]
|
||||||
|
keyring-tests = []
|
||||||
|
|
||||||
## Use the built-in Keychain Services on macOS and iOS
|
## Use the built-in Keychain Services on macOS
|
||||||
apple-native = ["dep:security-framework"]
|
apple-native = ["dep:security-framework"]
|
||||||
## Use the secret-service on *nix.
|
## Use the secret-service on *nix.
|
||||||
secret-service = ["dep:secret-service"]
|
secret-service = ["dep:secret-service"]
|
||||||
|
|
@ -21,10 +22,9 @@ windows-native = ["dep:windows-sys", "dep:byteorder"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
log = "0.4"
|
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
security-framework = { version = "3", optional = true }
|
security-framework = { version = "3", optional = true }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "linux",target_os = "freebsd", target_os = "openbsd"))'.dependencies]
|
[target.'cfg(any(target_os = "linux",target_os = "freebsd", target_os = "openbsd"))'.dependencies]
|
||||||
|
|
@ -35,26 +35,11 @@ byteorder = { version = "1", optional = true }
|
||||||
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security_Credentials"], optional = true }
|
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security_Credentials"], optional = true }
|
||||||
zeroize = "1.8.1"
|
zeroize = "1.8.1"
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "iostest"
|
|
||||||
path = "examples/ios.rs"
|
|
||||||
crate-type = ["staticlib"]
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "keyring-cli"
|
|
||||||
path = "examples/cli.rs"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
base64 = { workspace = true }
|
|
||||||
clap = { workspace = true }
|
|
||||||
doc-comment = "0.3"
|
doc-comment = "0.3"
|
||||||
env_logger = "0.11.5"
|
env_logger = "0.11.5"
|
||||||
fastrand = "2"
|
fastrand = "2"
|
||||||
rpassword = "7"
|
|
||||||
rprompt = "2"
|
|
||||||
tokio = { workspace = true }
|
|
||||||
whoami = "1.5"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
default-target = "x86_64-unknown-linux-gnu"
|
default-target = "x86_64-unknown-linux-gnu"
|
||||||
targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "aarch64-apple-ios", "x86_64-pc-windows-msvc"]
|
targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "x86_64-pc-windows-msvc"]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,22 @@
|
||||||
# uv-keyring
|
# uv-keyring
|
||||||
|
|
||||||
This is vendored from [keyring-rs crate](https://github.com/open-source-cooperative/keyring-rs) commit 9635a2f53a19eb7f188cdc4e38982dcb19caee00.
|
This is vendored from [keyring-rs crate](https://github.com/open-source-cooperative/keyring-rs)
|
||||||
|
commit 9635a2f53a19eb7f188cdc4e38982dcb19caee00.
|
||||||
|
|
||||||
A cross-platform library to manage storage and retrieval of passwords (and other secrets) in the underlying platform secure store, with a fully-developed example that provides a command-line interface.
|
A cross-platform library to manage storage and retrieval of passwords (and other secrets) in the
|
||||||
|
underlying platform secure store, with a fully-developed example that provides a command-line
|
||||||
|
interface.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can use the `Entry::new` function to create a new keyring entry. The `new` function takes a service name and a user's name which together identify the entry.
|
You can use the `Entry::new` function to create a new keyring entry. The `new` function takes a
|
||||||
|
service name and a user's name which together identify the entry.
|
||||||
|
|
||||||
Passwords (strings) or secrets (binary data) can be added to an entry using its `set_password` or `set_secret` methods, respectively. (These methods create or update an entry in the underlying platform's persistent credential store.) The password or secret can then be read back using the `get_password` or `get_secret` methods. The underlying credential (with its password/secret data) can then be removed using the `delete_credential` method.
|
Passwords (strings) or secrets (binary data) can be added to an entry using its `set_password` or
|
||||||
|
`set_secret` methods, respectively. (These methods create or update an entry in the underlying
|
||||||
|
platform's persistent credential store.) The password or secret can then be read back using the
|
||||||
|
`get_password` or `get_secret` methods. The underlying credential (with its password/secret data)
|
||||||
|
can then be removed using the `delete_credential` method.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use keyring::{Entry, Result};
|
use keyring::{Entry, Result};
|
||||||
|
|
@ -25,56 +33,32 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
Creating and operating on entries can yield a `keyring::Error` which provides both a platform-independent code that classifies the error and, where relevant, underlying platform errors or more information about what went wrong.
|
Creating and operating on entries can yield a `keyring::Error` which provides both a
|
||||||
|
platform-independent code that classifies the error and, where relevant, underlying platform errors
|
||||||
## Examples
|
or more information about what went wrong.
|
||||||
|
|
||||||
The keychain-rs project contains a sample application (`keyring-cli`) and a sample library (`ios`).
|
|
||||||
|
|
||||||
The `keyring-cli` application is a command-line interface to the full functionality of the keyring. Invoke it without arguments to see usage information. It handles binary data input and output using base64 encoding. It can be installed using `cargo install` and used to experiment with library functionality. It can also be used when debugging keyring-based applications to probe the contents of the credential store.
|
|
||||||
|
|
||||||
The `ios` library is a full exercise of all the iOS functionality; it's meant to be loaded into an iOS test harness such as the one found in [this project](https://github.com/brotskydotcom/rust-on-ios).
|
|
||||||
|
|
||||||
## Client Testing
|
|
||||||
|
|
||||||
This crate comes with a mock credential store that can be used by clients who want to test without accessing the native platform store. The mock store is cross-platform and allows mocking errors as well as successes. See the [developer docs](https://docs.rs/keyring/) for details.
|
|
||||||
|
|
||||||
## Extensibility
|
|
||||||
|
|
||||||
This crate allows clients to bring their own credential store by providing traits that clients can implement. See the [developer docs](https://docs.rs/keyring/) for details.
|
|
||||||
|
|
||||||
## Platforms
|
## Platforms
|
||||||
|
|
||||||
This crate provides built-in implementations of the following platform-specific credential stores:
|
This crate provides built-in implementations of the following platform-specific credential stores:
|
||||||
|
|
||||||
* _Linux_, _FreeBSD_, _OpenBSD_: The DBus-based Secret Service.
|
- _Linux_, _FreeBSD_, _OpenBSD_: The DBus-based Secret Service.
|
||||||
* _macOS_, _iOS_: Keychain Services.
|
- _macOS_: Keychain Services.
|
||||||
* _Windows_: The Windows Credential Manager.
|
- _Windows_: The Windows Credential Manager.
|
||||||
|
|
||||||
It can be built and used on other platforms, but will not provide a built-in credential store implementation; you will have to bring your own.
|
It can be built and used on other platforms, but will not provide a built-in credential store
|
||||||
|
implementation; you will have to bring your own.
|
||||||
|
|
||||||
### Platform-specific issues
|
### Platform-specific issues
|
||||||
|
|
||||||
Since neither the maintainers nor GitHub do testing on BSD variants, we rely on contributors to support these platforms. Thanks for your help!
|
If you use the _Secret Service_ as your credential store, be aware that every call to the Secret
|
||||||
|
Service is done via an inter-process call, which takes time (typically tens if not hundreds of
|
||||||
|
milliseconds).
|
||||||
|
|
||||||
If you use the *Secret Service* as your credential store, be aware of the following:
|
If you use the _Windows-native credential store_, be careful about multi-threaded access, because
|
||||||
|
the Windows credential store does not guarantee your calls will be serialized in the order they are
|
||||||
|
made. Always access any single credential from just one thread at a time, and if you are doing
|
||||||
|
operations on multiple credentials that require a particular serialization order, perform all those
|
||||||
|
operations from the same thread.
|
||||||
|
|
||||||
* The default build of this crate expects that `libdbus` will be installed on users' machines. If you have users whose machines might not have `libdbus` installed, you can specify the `vendored` feature when building this crate to statically link the dbus library with your app.
|
The _macOS credential store_ does not allow service names or usernames to be empty, because empty
|
||||||
* Every call to the Secret Service is done via an inter-process call, which takes time (typically tens if not hundreds of milliseconds).
|
fields are treated as wildcards on lookup. Use some default, non-empty value instead.
|
||||||
* By default, this implementation does not encrypt secrets when sending them to or fetching them from the Dbus. If you want them encrypted, you can specify the `encrypted` feature when building this crate.
|
|
||||||
|
|
||||||
If you use the *Windows-native credential store*, be careful about multi-threaded access, because the Windows credential store does not guarantee your calls will be serialized in the order they are made. Always access any single credential from just one thread at a time, and if you are doing operations on multiple credentials that require a particular serialization order, perform all those operations from the same thread.
|
|
||||||
|
|
||||||
The *macOS and iOS credential stores* do not allow service names or usernames to be empty, because empty fields are treated as wildcards on lookup. Use some default, non-empty value instead.
|
|
||||||
|
|
||||||
## Upgrading from v3
|
|
||||||
|
|
||||||
There are no functional API changes between v4 and v3. All the changes are in the keystore implementations and how features are used to select keystores:
|
|
||||||
|
|
||||||
* Version 4 of this crate removes a number of the built-in credential stores that were available in version 3, namely the async secret service and linux keyutils. These keystores are being contributed directly to the existing [secret-service](https://crates.io/crates/secret-service) and [linux-keyutils](https://crates.io/crates/linux-keyutils) crates, respectively.
|
|
||||||
* Version 4 of this crate dispenses with the need to explicitly specify which credential store you want to use on each platform. Instead, the default feature set provides a single credential store on each platform. If you would rather bring your own store, and not build this crate's built-in ones, you can simply suppress the default feature set.
|
|
||||||
* The built-in macOS keystore now supports use of the Data Protection keychain, which is the same keychain used by iOS. You can specify a target of "Data Protection" (or simply "Protected") to write and read credentials in that keychain.
|
|
||||||
|
|
||||||
All v2/v3 data is fully forward-compatible with v4 data; there have been no changes at all in that respect.
|
|
||||||
|
|
||||||
The Rust edition of this crate has moved to 2024, and the MSRV has moved to 1.85.
|
|
||||||
|
|
|
||||||
|
|
@ -1,336 +0,0 @@
|
||||||
extern crate uv_keyring;
|
|
||||||
|
|
||||||
use clap::{Args, Parser};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use uv_keyring::{Entry, Error, Result};
|
|
||||||
|
|
||||||
#[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()
|
|
||||||
}
|
|
||||||
let entry = match args.entry_for() {
|
|
||||||
Ok(entry) => entry,
|
|
||||||
Err(err) => {
|
|
||||||
if args.verbose {
|
|
||||||
let description = args.description();
|
|
||||||
eprintln!("Couldn't create entry for '{description}': {err}")
|
|
||||||
}
|
|
||||||
std::process::exit(1)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match &args.command {
|
|
||||||
Command::Set { .. } => {
|
|
||||||
let value = args.get_password_and_attributes();
|
|
||||||
match &value {
|
|
||||||
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).await {
|
|
||||||
Ok(()) => args.success_message_for(&value),
|
|
||||||
Err(err) => args.error_message_for(err),
|
|
||||||
},
|
|
||||||
Value::Attributes(attributes) => {
|
|
||||||
let attrs: HashMap<&str, &str> = attributes
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
|
||||||
.collect();
|
|
||||||
match entry.update_attributes(&attrs).await {
|
|
||||||
Ok(()) => args.success_message_for(&value),
|
|
||||||
Err(err) => args.error_message_for(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => panic!("Can't set without a value"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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().await {
|
|
||||||
Ok(secret) => {
|
|
||||||
println!("{}", secret_string(&secret));
|
|
||||||
args.success_message_for(&Value::Secret(secret.to_vec()));
|
|
||||||
}
|
|
||||||
Err(err) => args.error_message_for(err),
|
|
||||||
},
|
|
||||||
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().await {
|
|
||||||
Ok(()) => args.success_message_for(&Value::None),
|
|
||||||
Err(err) => args.error_message_for(err),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
#[clap(author = "github.com/hwchen/keyring-rs")]
|
|
||||||
/// Keyring CLI: A command-line interface to platform secure storage
|
|
||||||
pub struct Cli {
|
|
||||||
#[clap(short, long, action, verbatim_doc_comment)]
|
|
||||||
/// Write debugging info to stderr, including retrieved passwords and secrets.
|
|
||||||
/// If an operation fails, detailed error information is provided.
|
|
||||||
pub verbose: bool,
|
|
||||||
|
|
||||||
#[clap(short, long, value_parser)]
|
|
||||||
/// The (optional) target for the entry.
|
|
||||||
pub target: Option<String>,
|
|
||||||
|
|
||||||
#[clap(short, long, value_parser, default_value = "keyring-cli")]
|
|
||||||
/// The service for the entry.
|
|
||||||
pub service: String,
|
|
||||||
|
|
||||||
#[clap(short, long, value_parser, default_value = "<logged-in username>")]
|
|
||||||
/// The user for the entry.
|
|
||||||
pub user: String,
|
|
||||||
|
|
||||||
#[clap(subcommand)]
|
|
||||||
pub command: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
pub enum Command {
|
|
||||||
/// Set the password or update the attributes in the secure store
|
|
||||||
Set {
|
|
||||||
#[command(flatten)]
|
|
||||||
what: What,
|
|
||||||
|
|
||||||
#[clap(value_parser)]
|
|
||||||
/// The input to parse. If not specified, it will be
|
|
||||||
/// read interactively from the terminal. Password/secret
|
|
||||||
/// input will not be echoed.
|
|
||||||
input: Option<String>,
|
|
||||||
},
|
|
||||||
/// Retrieve the (string) password from the secure store
|
|
||||||
/// and write it to the standard output.
|
|
||||||
Password,
|
|
||||||
/// Retrieve the (binary) secret from the secure store
|
|
||||||
/// and write it in base64 encoding to the standard output.
|
|
||||||
Secret,
|
|
||||||
/// Retrieve attributes available in the secure store.
|
|
||||||
Attributes,
|
|
||||||
/// Delete the credential from the secure store.
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
|
||||||
#[group(multiple = false, required = true)]
|
|
||||||
pub struct What {
|
|
||||||
#[clap(short, long, action, help = "The input is a password")]
|
|
||||||
password: bool,
|
|
||||||
|
|
||||||
#[clap(short, long, action, help = "The input is a base64-encoded secret")]
|
|
||||||
secret: bool,
|
|
||||||
|
|
||||||
#[clap(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
action,
|
|
||||||
help = "The input is comma-separated, key=val attribute pairs"
|
|
||||||
)]
|
|
||||||
attributes: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Value {
|
|
||||||
Secret(Vec<u8>),
|
|
||||||
Password(String),
|
|
||||||
Attributes(HashMap<String, String>),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
fn description(&self) -> String {
|
|
||||||
if let Some(target) = &self.target {
|
|
||||||
format!("{}@{}:{target}", &self.user, &self.service)
|
|
||||||
} else {
|
|
||||||
format!("{}@{}", &self.user, &self.service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn entry_for(&self) -> Result<Entry> {
|
|
||||||
if let Some(target) = &self.target {
|
|
||||||
Entry::new_with_target(target, &self.service, &self.user)
|
|
||||||
} else {
|
|
||||||
Entry::new(&self.service, &self.user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_message_for(&self, err: Error) {
|
|
||||||
if self.verbose {
|
|
||||||
let description = self.description();
|
|
||||||
match err {
|
|
||||||
Error::NoEntry => {
|
|
||||||
eprintln!("No credential found for '{description}'");
|
|
||||||
}
|
|
||||||
Error::Ambiguous(creds) => {
|
|
||||||
eprintln!("More than one credential found for '{description}': {creds:?}");
|
|
||||||
}
|
|
||||||
err => match self.command {
|
|
||||||
Command::Set { .. } => {
|
|
||||||
eprintln!("Couldn't set credential data for '{description}': {err}");
|
|
||||||
}
|
|
||||||
Command::Password => {
|
|
||||||
eprintln!("Couldn't get password for '{description}': {err}");
|
|
||||||
}
|
|
||||||
Command::Secret => {
|
|
||||||
eprintln!("Couldn't get secret for '{description}': {err}");
|
|
||||||
}
|
|
||||||
Command::Attributes => {
|
|
||||||
eprintln!("Couldn't get attributes for '{description}': {err}");
|
|
||||||
}
|
|
||||||
Command::Delete => {
|
|
||||||
eprintln!("Couldn't delete credential for '{description}': {err}");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::process::exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn success_message_for(&self, value: &Value) {
|
|
||||||
if !self.verbose {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let description = self.description();
|
|
||||||
match self.command {
|
|
||||||
Command::Set { .. } => match value {
|
|
||||||
Value::Secret(secret) => {
|
|
||||||
let secret = secret_string(secret);
|
|
||||||
eprintln!("Set secret for '{description}' to decode of '{secret}'");
|
|
||||||
}
|
|
||||||
Value::Password(password) => {
|
|
||||||
eprintln!("Set password for '{description}' to '{password}'");
|
|
||||||
}
|
|
||||||
Value::Attributes(attributes) => {
|
|
||||||
eprintln!("The following attributes for '{description}' were sent for update:");
|
|
||||||
eprint_attributes(attributes);
|
|
||||||
}
|
|
||||||
_ => panic!("Can't set without a value"),
|
|
||||||
},
|
|
||||||
Command::Password => {
|
|
||||||
match value {
|
|
||||||
Value::Password(password) => {
|
|
||||||
eprintln!("Password for '{description}' is '{password}'");
|
|
||||||
}
|
|
||||||
_ => panic!("Wrong value type for command"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Command::Secret => match value {
|
|
||||||
Value::Secret(secret) => {
|
|
||||||
let encoded = secret_string(secret);
|
|
||||||
eprintln!("Secret for '{description}' encodes as {encoded}");
|
|
||||||
}
|
|
||||||
_ => panic!("Wrong value type for command"),
|
|
||||||
},
|
|
||||||
Command::Attributes => match value {
|
|
||||||
Value::Attributes(attributes) => {
|
|
||||||
if attributes.is_empty() {
|
|
||||||
eprintln!("No attributes found for '{description}'");
|
|
||||||
} else {
|
|
||||||
eprintln!("Attributes for '{description}' are:");
|
|
||||||
eprint_attributes(attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => panic!("Wrong value type for command"),
|
|
||||||
},
|
|
||||||
Command::Delete => {
|
|
||||||
eprintln!("Successfully deleted credential for '{description}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_password_and_attributes(&self) -> Value {
|
|
||||||
if let Command::Set { what, input } = &self.command {
|
|
||||||
if what.password {
|
|
||||||
Value::Password(read_password(input))
|
|
||||||
} else if what.secret {
|
|
||||||
Value::Secret(decode_secret(input))
|
|
||||||
} else {
|
|
||||||
Value::Attributes(parse_attributes(input))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("Can't happen: asking for password and attributes on non-set command")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn secret_string(secret: &[u8]) -> String {
|
|
||||||
use base64::prelude::*;
|
|
||||||
|
|
||||||
BASE64_STANDARD.encode(secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eprint_attributes(attributes: &HashMap<String, String>) {
|
|
||||||
for (key, value) in attributes {
|
|
||||||
println!(" {key}: {value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_secret(input: &Option<String>) -> Vec<u8> {
|
|
||||||
use base64::prelude::*;
|
|
||||||
|
|
||||||
let encoded = if let Some(input) = input {
|
|
||||||
input.clone()
|
|
||||||
} else {
|
|
||||||
rpassword::prompt_password("Base64 encoding: ").unwrap_or_else(|_| String::new())
|
|
||||||
};
|
|
||||||
if encoded.is_empty() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
match BASE64_STANDARD.decode(encoded) {
|
|
||||||
Ok(secret) => secret,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Sorry, the provided secret data is not base64-encoded: {err}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_password(input: &Option<String>) -> String {
|
|
||||||
if let Some(input) = input {
|
|
||||||
input.clone()
|
|
||||||
} else {
|
|
||||||
rpassword::prompt_password("Password: ").unwrap_or_else(|_| String::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attributes_string(attributes: &HashMap<String, String>) -> String {
|
|
||||||
let strings = attributes
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, v))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
strings.join(",")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_attributes(input: &Option<String>) -> HashMap<String, String> {
|
|
||||||
let input = if let Some(input) = input {
|
|
||||||
input.clone()
|
|
||||||
} else {
|
|
||||||
rprompt::prompt_reply("Attributes: ").unwrap_or_else(|_| String::new())
|
|
||||||
};
|
|
||||||
if input.is_empty() {
|
|
||||||
eprintln!("You must specify at least one key=value attribute pair to set")
|
|
||||||
}
|
|
||||||
let mut attributes = HashMap::new();
|
|
||||||
let parts = input.split(',');
|
|
||||||
for s in parts.into_iter() {
|
|
||||||
let parts: Vec<&str> = s.split("=").collect();
|
|
||||||
if parts.len() != 2 || parts[0].is_empty() {
|
|
||||||
eprintln!("Sorry, this part of the attributes string is not a key=val pair: {s}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
attributes.insert(parts[0].to_string(), parts[1].to_string());
|
|
||||||
}
|
|
||||||
attributes
|
|
||||||
}
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
use uv_keyring::{Entry, Error};
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
extern "C" fn test() {
|
|
||||||
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() {
|
|
||||||
let entry = Entry::new("", "user");
|
|
||||||
assert!(
|
|
||||||
matches!(entry, Err(Error::Invalid(_, _))),
|
|
||||||
"Created entry with empty service"
|
|
||||||
);
|
|
||||||
let entry = Entry::new("service", "");
|
|
||||||
assert!(
|
|
||||||
matches!(entry, Err(Error::Invalid(_, _))),
|
|
||||||
"Created entry with empty user"
|
|
||||||
);
|
|
||||||
let entry = Entry::new_with_target("test", "service", "user");
|
|
||||||
assert!(
|
|
||||||
matches!(entry, Err(Error::Invalid(_, _))),
|
|
||||||
"Created entry with non-default target"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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().await, Err(Error::NoEntry)))
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
.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().await, Err(Error::NoEntry)),
|
|
||||||
"Able to read a deleted 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).await.unwrap();
|
|
||||||
let stored_password = entry.get_password().await.unwrap();
|
|
||||||
assert_eq!(stored_password, password);
|
|
||||||
entry.delete_credential().await.unwrap();
|
|
||||||
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)))
|
|
||||||
}
|
|
||||||
|
|
||||||
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).await.unwrap();
|
|
||||||
let stored_password = entry.get_password().await.unwrap();
|
|
||||||
assert_eq!(stored_password, password);
|
|
||||||
entry.delete_credential().await.unwrap();
|
|
||||||
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)))
|
|
||||||
}
|
|
||||||
|
|
||||||
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).await.unwrap();
|
|
||||||
let stored_password = entry.get_password().await.unwrap();
|
|
||||||
assert_eq!(stored_password, password);
|
|
||||||
let password = "このきれいな花は桜です";
|
|
||||||
entry.set_password(password).await.unwrap();
|
|
||||||
let stored_password = entry.get_password().await.unwrap();
|
|
||||||
assert_eq!(stored_password, password);
|
|
||||||
entry.delete_credential().await.unwrap();
|
|
||||||
assert!(matches!(entry.get_password().await, Err(Error::NoEntry)))
|
|
||||||
}
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
# Platform-independent secure storage model
|
# Platform-independent secure storage model
|
||||||
|
|
||||||
This module defines a plug and play model for platform-specific credential stores.
|
This module defines a plug and play model for platform-specific credential stores.
|
||||||
The model comprises two traits: [CredentialBuilderApi] for the underlying store
|
The model comprises two traits: [`CredentialBuilderApi`] for the underlying store
|
||||||
and [CredentialApi] for the entries in the store. These traits must be implemented
|
and [`CredentialApi`] for the entries in the store. These traits must be implemented
|
||||||
in a thread-safe way, a requirement captured in the [CredentialBuilder] and
|
in a thread-safe way, a requirement captured in the [`CredentialBuilder`] and
|
||||||
[Credential] types that wrap them.
|
[`Credential`] types that wrap them.
|
||||||
*/
|
*/
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -117,7 +117,7 @@ impl std::fmt::Debug for Credential {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A descriptor for the lifetime of stored credentials, returned from
|
/// A descriptor for the lifetime of stored credentials, returned from
|
||||||
/// a credential store's [persistence](CredentialBuilderApi::persistence) call.
|
/// a credential store's [`persistence`](CredentialBuilderApi::persistence) call.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum CredentialPersistence {
|
pub enum CredentialPersistence {
|
||||||
/// Credentials vanish when the entry vanishes (stored in the entry)
|
/// Credentials vanish when the entry vanishes (stored in the entry)
|
||||||
|
|
@ -161,7 +161,7 @@ impl std::fmt::Debug for CredentialBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A thread-safe implementation of the [CredentialBuilder API](CredentialBuilderApi).
|
/// A thread-safe implementation of the [`CredentialBuilder` API](CredentialBuilderApi).
|
||||||
pub type CredentialBuilder = dyn CredentialBuilderApi + Send + Sync;
|
pub type CredentialBuilder = dyn CredentialBuilderApi + Send + Sync;
|
||||||
|
|
||||||
struct NopCredentialBuilder;
|
struct NopCredentialBuilder;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use crate::Credential;
|
||||||
/// which may be platform-specific.
|
/// which may be platform-specific.
|
||||||
///
|
///
|
||||||
/// This enum is non-exhaustive so that more values can be added to it
|
/// This enum is non-exhaustive so that more values can be added to it
|
||||||
/// without a SemVer break. Clients should always have default handling
|
/// without a `SemVer` break. Clients should always have default handling
|
||||||
/// for variants they don't understand.
|
/// for variants they don't understand.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
@ -64,27 +64,27 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||||
impl std::fmt::Display for Error {
|
impl std::fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Error::PlatformFailure(err) => write!(f, "Platform secure storage failure: {err}"),
|
Self::PlatformFailure(err) => write!(f, "Platform secure storage failure: {err}"),
|
||||||
Error::NoStorageAccess(err) => {
|
Self::NoStorageAccess(err) => {
|
||||||
write!(f, "Couldn't access platform secure storage: {err}")
|
write!(f, "Couldn't access platform secure storage: {err}")
|
||||||
}
|
}
|
||||||
Error::NoEntry => write!(f, "No matching entry found in secure storage"),
|
Self::NoEntry => write!(f, "No matching entry found in secure storage"),
|
||||||
Error::BadEncoding(_) => write!(f, "Data is not UTF-8 encoded"),
|
Self::BadEncoding(_) => write!(f, "Data is not UTF-8 encoded"),
|
||||||
Error::TooLong(name, len) => write!(
|
Self::TooLong(name, len) => write!(
|
||||||
f,
|
f,
|
||||||
"Attribute '{name}' is longer than platform limit of {len} chars"
|
"Attribute '{name}' is longer than platform limit of {len} chars"
|
||||||
),
|
),
|
||||||
Error::Invalid(attr, reason) => {
|
Self::Invalid(attr, reason) => {
|
||||||
write!(f, "Attribute {attr} is invalid: {reason}")
|
write!(f, "Attribute {attr} is invalid: {reason}")
|
||||||
}
|
}
|
||||||
Error::Ambiguous(items) => {
|
Self::Ambiguous(items) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Entry is matched by {} credentials: {items:?}",
|
"Entry is matched by {} credentials: {items:?}",
|
||||||
items.len(),
|
items.len(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Error::NoDefaultCredentialBuilder => {
|
Self::NoDefaultCredentialBuilder => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"No default credential builder is available; set one before creating entries"
|
"No default credential builder is available; set one before creating entries"
|
||||||
|
|
@ -97,8 +97,8 @@ impl std::fmt::Display for Error {
|
||||||
impl std::error::Error for Error {
|
impl std::error::Error for Error {
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
Error::PlatformFailure(err) => Some(err.as_ref()),
|
Self::PlatformFailure(err) => Some(err.as_ref()),
|
||||||
Error::NoStorageAccess(err) => Some(err.as_ref()),
|
Self::NoStorageAccess(err) => Some(err.as_ref()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Linux,
|
||||||
FreeBSD,
|
FreeBSD,
|
||||||
OpenBSD,
|
OpenBSD,
|
||||||
Windows,
|
Windows,
|
||||||
macOS, and iOS.
|
and macOS.
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
|
|
@ -33,13 +33,13 @@ as well as which values are allowed for _target_ by that store.
|
||||||
The abstract behavior of entries and credential stores are captured
|
The abstract behavior of entries and credential stores are captured
|
||||||
by two types (with associated traits):
|
by two types (with associated traits):
|
||||||
|
|
||||||
- a _credential builder_, represented by the [CredentialBuilder] type
|
- a _credential builder_, represented by the [`CredentialBuilder`] type
|
||||||
(and [CredentialBuilderApi](credential::CredentialBuilderApi) trait). Credential
|
(and [`CredentialBuilderApi`](credential::CredentialBuilderApi) trait). Credential
|
||||||
builders are given the identifying information (and target, if any)
|
builders are given the identifying information (and target, if any)
|
||||||
provided for an entry and map
|
provided for an entry and map
|
||||||
it to the identifying information for a platform-specific credential.
|
it to the identifying information for a platform-specific credential.
|
||||||
- a _credential_, represented by the [Credential] type
|
- a _credential_, represented by the [`Credential`] type
|
||||||
(and [CredentialApi](credential::CredentialApi) trait). The platform-specific credential
|
(and [`CredentialApi`](credential::CredentialApi) trait). The platform-specific credential
|
||||||
identified by the builder for an entry is what provides the secure storage
|
identified by the builder for an entry is what provides the secure storage
|
||||||
for that entry's password/secret.
|
for that entry's password/secret.
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ by two types (with associated traits):
|
||||||
|
|
||||||
This crate runs on several different platforms, and on each one
|
This crate runs on several different platforms, and on each one
|
||||||
it provides (by default) an implementation of a default credential store used
|
it provides (by default) an implementation of a default credential store used
|
||||||
on that platform (see [default_credential_builder]).
|
on that platform (see [`default_credential_builder`]).
|
||||||
These implementations work by mapping the data used to identify an entry
|
These implementations work by mapping the data used to identify an entry
|
||||||
to data used to identify platform-specific storage objects.
|
to data used to identify platform-specific storage objects.
|
||||||
For example, on macOS, the service and user provided for an entry
|
For example, on macOS, the service and user provided for an entry
|
||||||
|
|
@ -60,9 +60,9 @@ the one used by this crate to identify entries.
|
||||||
These keystores expose their specific model in the
|
These keystores expose their specific model in the
|
||||||
concrete credential objects they use to implement the Credential trait.
|
concrete credential objects they use to implement the Credential trait.
|
||||||
In order to allow clients to access this richer model, the Credential trait
|
In order to allow clients to access this richer model, the Credential trait
|
||||||
has an [as_any](credential::CredentialApi::as_any) method that returns a
|
has an [`as_any`](credential::CredentialApi::as_any) method that returns a
|
||||||
reference to the underlying
|
reference to the underlying
|
||||||
concrete object typed as [Any](std::any::Any), so that it can be downgraded to
|
concrete object typed as [`Any`](std::any::Any), so that it can be downgraded to
|
||||||
its concrete type.
|
its concrete type.
|
||||||
|
|
||||||
### Credential store features
|
### Credential store features
|
||||||
|
|
@ -76,23 +76,15 @@ is built with a macOS target.
|
||||||
The available credential store features, listed here, are all included in the
|
The available credential store features, listed here, are all included in the
|
||||||
default feature set:
|
default feature set:
|
||||||
|
|
||||||
- `apple-native`: Provides access to the Keychain credential store on macOS and iOS.
|
- `apple-native`: Provides access to the Keychain credential store on macOS.
|
||||||
|
|
||||||
- `windows-native`: Provides access to the Windows Credential Store on Windows.
|
- `windows-native`: Provides access to the Windows Credential Store on Windows.
|
||||||
|
|
||||||
- `secret-service`: Provides access to the DBus-based
|
- `secret-service`: Provides access to Secret Service.
|
||||||
[Secret Service](https://specifications.freedesktop.org/secret-service/latest/)
|
|
||||||
storage on Linux, FreeBSD, and OpenBSD. This keystore provides
|
|
||||||
support for encrypting secrets when they are transferred across the bus,
|
|
||||||
specify the `encrypted` feature if you want to use this support.
|
|
||||||
By default, this keystore requires that the DBus library be
|
|
||||||
installed on the user's machine,
|
|
||||||
but you can avoid this requirement by specifying the `vendored` feature
|
|
||||||
(which will cause the build to include a static build of the dbus library).
|
|
||||||
|
|
||||||
If you suppress the default feature set when building this crate, and you
|
If you suppress the default feature set when building this crate, and you
|
||||||
don't separately specify one of the included keystore features for your platform,
|
don't separately specify one of the included keystore features for your platform,
|
||||||
then no keystore will be built in, and calls to [Entry::new] and [Entry::new_with_target]
|
then no keystore will be built in, and calls to [`Entry::new`] and [`Entry::new_with_target`]
|
||||||
will fail unless the client brings their own keystore (see next section).
|
will fail unless the client brings their own keystore (see next section).
|
||||||
|
|
||||||
## Client-provided Credential Stores
|
## Client-provided Credential Stores
|
||||||
|
|
@ -102,13 +94,13 @@ are free to provide their own keystores and use those. There are
|
||||||
two mechanisms provided for this:
|
two mechanisms provided for this:
|
||||||
|
|
||||||
- Clients can give their desired credential builder to the crate
|
- Clients can give their desired credential builder to the crate
|
||||||
for use by the [Entry::new] and [Entry::new_with_target] calls.
|
for use by the [`Entry::new`] and [`Entry::new_with_target`] calls.
|
||||||
This is done by making a call to [set_default_credential_builder].
|
This is done by making a call to [`set_default_credential_builder`].
|
||||||
The major advantage of this approach is that client code remains
|
The major advantage of this approach is that client code remains
|
||||||
independent of the credential builder being used.
|
independent of the credential builder being used.
|
||||||
|
|
||||||
- Clients can construct their concrete credentials directly and
|
- Clients can construct their concrete credentials directly and
|
||||||
then turn them into entries by using the [Entry::new_with_credential]
|
then turn them into entries by using the [`Entry::new_with_credential`]
|
||||||
call. The major advantage of this approach is that credentials
|
call. The major advantage of this approach is that credentials
|
||||||
can be identified however clients want, rather than being restricted
|
can be identified however clients want, rather than being restricted
|
||||||
to the simple model used by this crate.
|
to the simple model used by this crate.
|
||||||
|
|
@ -119,7 +111,7 @@ In addition to the platform-specific credential stores, this crate
|
||||||
always provides a mock credential store that clients can use to
|
always provides a mock credential store that clients can use to
|
||||||
test their code in a platform independent way. The mock credential
|
test their code in a platform independent way. The mock credential
|
||||||
store allows for pre-setting errors as well as password values to
|
store allows for pre-setting errors as well as password values to
|
||||||
be returned from [Entry] method calls. If you want to use the mock
|
be returned from [`Entry`] method calls. If you want to use the mock
|
||||||
credential store as your default in tests, make this call:
|
credential store as your default in tests, make this call:
|
||||||
```
|
```
|
||||||
uv_keyring::set_default_credential_builder(uv_keyring::mock::default_credential_builder())
|
uv_keyring::set_default_credential_builder(uv_keyring::mock::default_credential_builder())
|
||||||
|
|
@ -145,11 +137,11 @@ view the storage module documentation for your desired platform.)
|
||||||
This module expects passwords to be UTF-8 encoded strings,
|
This module expects passwords to be UTF-8 encoded strings,
|
||||||
so if a third party has stored an arbitrary byte string
|
so if a third party has stored an arbitrary byte string
|
||||||
then retrieving that as a password will return a
|
then retrieving that as a password will return a
|
||||||
[BadEncoding](Error::BadEncoding) error.
|
[`BadEncoding`](Error::BadEncoding) error.
|
||||||
The returned error will have the raw bytes attached,
|
The returned error will have the raw bytes attached,
|
||||||
so you can access them, but you can also just fetch
|
so you can access them, but you can also just fetch
|
||||||
them directly using [get_secret](Entry::get_secret) rather than
|
them directly using [`get_secret`](Entry::get_secret) rather than
|
||||||
[get_password](Entry::get_password).
|
[`get_password`](Entry::get_password).
|
||||||
|
|
||||||
While this crate's code is thread-safe, the underlying credential
|
While this crate's code is thread-safe, the underlying credential
|
||||||
stores may not handle access from different threads reliably.
|
stores may not handle access from different threads reliably.
|
||||||
|
|
@ -161,7 +153,6 @@ Service, accesses from multiple threads (and even the same thread very quickly)
|
||||||
are not recommended, as they may cause the RPC mechanism to fail.
|
are not recommended, as they may cause the RPC mechanism to fail.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub use credential::{Credential, CredentialBuilder};
|
pub use credential::{Credential, CredentialBuilder};
|
||||||
|
|
@ -212,7 +203,7 @@ static DEFAULT_BUILDER: std::sync::RwLock<EntryBuilder> =
|
||||||
/// This is really meant for use by clients who bring their own credential
|
/// This is really meant for use by clients who bring their own credential
|
||||||
/// store and want to use it everywhere. If you are using multiple credential
|
/// store and want to use it everywhere. If you are using multiple credential
|
||||||
/// stores and want precise control over which credential is in which store,
|
/// stores and want precise control over which credential is in which store,
|
||||||
/// then use [new_with_credential](Entry::new_with_credential).
|
/// then use [`new_with_credential`](Entry::new_with_credential).
|
||||||
///
|
///
|
||||||
/// This will block waiting for all other threads currently creating entries
|
/// This will block waiting for all other threads currently creating entries
|
||||||
/// to complete what they are doing. It's really meant to be called
|
/// to complete what they are doing. It's really meant to be called
|
||||||
|
|
@ -233,8 +224,6 @@ pub fn default_credential_builder() -> Box<CredentialBuilder> {
|
||||||
return secret_service::default_credential_builder();
|
return secret_service::default_credential_builder();
|
||||||
#[cfg(all(target_os = "macos", feature = "apple-native"))]
|
#[cfg(all(target_os = "macos", feature = "apple-native"))]
|
||||||
return macos::default_credential_builder();
|
return macos::default_credential_builder();
|
||||||
#[cfg(all(target_os = "ios", feature = "apple-native"))]
|
|
||||||
return ios::default_credential_builder();
|
|
||||||
#[cfg(all(target_os = "windows", feature = "windows-native"))]
|
#[cfg(all(target_os = "windows", feature = "windows-native"))]
|
||||||
return windows::default_credential_builder();
|
return windows::default_credential_builder();
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
|
|
@ -242,7 +231,6 @@ pub fn default_credential_builder() -> Box<CredentialBuilder> {
|
||||||
all(target_os = "freebsd", feature = "secret-service"),
|
all(target_os = "freebsd", feature = "secret-service"),
|
||||||
all(target_os = "openbsd", feature = "secret-service"),
|
all(target_os = "openbsd", feature = "secret-service"),
|
||||||
all(target_os = "macos", feature = "apple-native"),
|
all(target_os = "macos", feature = "apple-native"),
|
||||||
all(target_os = "ios", feature = "apple-native"),
|
|
||||||
all(target_os = "windows", feature = "windows-native"),
|
all(target_os = "windows", feature = "windows-native"),
|
||||||
)))]
|
)))]
|
||||||
credential::nop_credential_builder()
|
credential::nop_credential_builder()
|
||||||
|
|
@ -271,86 +259,77 @@ impl Entry {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// This function will return an [Error] if the `service` or `user` values are invalid.
|
/// This function will return an [`Error`] if the `service` or `user` values are invalid.
|
||||||
/// The specific reasons for invalidity are platform-dependent, but include length constraints.
|
/// The specific reasons for invalidity are platform-dependent, but include length constraints.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// In the very unlikely event that the internal credential builder's `RwLock`` is poisoned, this function
|
/// In the very unlikely event that the internal credential builder's `RwLock` is poisoned, this function
|
||||||
/// will panic. If you encounter this, and especially if you can reproduce it, please report a bug with the
|
/// will panic. If you encounter this, and especially if you can reproduce it, please report a bug with the
|
||||||
/// details (and preferably a backtrace) so the developers can investigate.
|
/// details (and preferably a backtrace) so the developers can investigate.
|
||||||
pub fn new(service: &str, user: &str) -> Result<Entry> {
|
pub fn new(service: &str, user: &str) -> Result<Self> {
|
||||||
debug!("creating entry with service {service}, user {user}, and no target");
|
|
||||||
let entry = build_default_credential(None, service, user)?;
|
let entry = build_default_credential(None, service, user)?;
|
||||||
debug!("created entry {:?}", entry.inner);
|
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an entry for the given target, service, and user.
|
/// Create an entry for the given target, service, and user.
|
||||||
///
|
///
|
||||||
/// The default credential builder is used.
|
/// The default credential builder is used.
|
||||||
pub fn new_with_target(target: &str, service: &str, user: &str) -> Result<Entry> {
|
pub fn new_with_target(target: &str, service: &str, user: &str) -> Result<Self> {
|
||||||
debug!("creating entry with service {service}, user {user}, and target {target}");
|
|
||||||
let entry = build_default_credential(Some(target), service, user)?;
|
let entry = build_default_credential(Some(target), service, user)?;
|
||||||
debug!("created entry {:?}", entry.inner);
|
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an entry from a credential that may be in any credential store.
|
/// Create an entry from a credential that may be in any credential store.
|
||||||
pub fn new_with_credential(credential: Box<Credential>) -> Entry {
|
pub fn new_with_credential(credential: Box<Credential>) -> Self {
|
||||||
debug!("create entry from {credential:?}");
|
Self { inner: credential }
|
||||||
Entry { inner: credential }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the password for this entry.
|
/// Set the password for this entry.
|
||||||
///
|
///
|
||||||
/// Can return an [Ambiguous](Error::Ambiguous) error
|
/// Can return an [`Ambiguous`](Error::Ambiguous) error
|
||||||
/// if there is more than one platform credential
|
/// if there is more than one platform credential
|
||||||
/// that matches this entry. This can only happen
|
/// that matches this entry. This can only happen
|
||||||
/// on some platforms, and then only if a third-party
|
/// on some platforms, and then only if a third-party
|
||||||
/// application wrote the ambiguous credential.
|
/// application wrote the ambiguous credential.
|
||||||
pub async 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).await
|
self.inner.set_password(password).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the secret for this entry.
|
/// Set the secret for this entry.
|
||||||
///
|
///
|
||||||
/// Can return an [Ambiguous](Error::Ambiguous) error
|
/// Can return an [`Ambiguous`](Error::Ambiguous) error
|
||||||
/// if there is more than one platform credential
|
/// if there is more than one platform credential
|
||||||
/// that matches this entry. This can only happen
|
/// that matches this entry. This can only happen
|
||||||
/// on some platforms, and then only if a third-party
|
/// on some platforms, and then only if a third-party
|
||||||
/// application wrote the ambiguous credential.
|
/// application wrote the ambiguous credential.
|
||||||
pub async 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).await
|
self.inner.set_secret(secret).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the password saved for this entry.
|
/// Retrieve the password saved for this entry.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](Error::NoEntry) error if there isn't one.
|
/// Returns a [`NoEntry`](Error::NoEntry) error if there isn't one.
|
||||||
///
|
///
|
||||||
/// Can return an [Ambiguous](Error::Ambiguous) error
|
/// Can return an [`Ambiguous`](Error::Ambiguous) error
|
||||||
/// if there is more than one platform credential
|
/// if there is more than one platform credential
|
||||||
/// that matches this entry. This can only happen
|
/// that matches this entry. This can only happen
|
||||||
/// on some platforms, and then only if a third-party
|
/// on some platforms, and then only if a third-party
|
||||||
/// application wrote the ambiguous credential.
|
/// application wrote the ambiguous credential.
|
||||||
pub async 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().await
|
self.inner.get_password().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the secret saved for this entry.
|
/// Retrieve the secret saved for this entry.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](Error::NoEntry) error if there isn't one.
|
/// Returns a [`NoEntry`](Error::NoEntry) error if there isn't one.
|
||||||
///
|
///
|
||||||
/// Can return an [Ambiguous](Error::Ambiguous) error
|
/// Can return an [`Ambiguous`](Error::Ambiguous) error
|
||||||
/// if there is more than one platform credential
|
/// if there is more than one platform credential
|
||||||
/// that matches this entry. This can only happen
|
/// that matches this entry. This can only happen
|
||||||
/// on some platforms, and then only if a third-party
|
/// on some platforms, and then only if a third-party
|
||||||
/// application wrote the ambiguous credential.
|
/// application wrote the ambiguous credential.
|
||||||
pub async 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().await
|
self.inner.get_secret().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -360,15 +339,14 @@ impl Entry {
|
||||||
/// that can be set to string values. See the documentation for each credential store
|
/// that can be set to string values. See the documentation for each credential store
|
||||||
/// for a list of which attribute names are supported by that store.
|
/// for a list of which attribute names are supported by that store.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](Error::NoEntry) error if there isn't a credential for this entry.
|
/// Returns a [`NoEntry`](Error::NoEntry) error if there isn't a credential for this entry.
|
||||||
///
|
///
|
||||||
/// Can return an [Ambiguous](Error::Ambiguous) error
|
/// Can return an [`Ambiguous`](Error::Ambiguous) error
|
||||||
/// if there is more than one platform credential
|
/// if there is more than one platform credential
|
||||||
/// that matches this entry. This can only happen
|
/// that matches this entry. This can only happen
|
||||||
/// on some platforms, and then only if a third-party
|
/// on some platforms, and then only if a third-party
|
||||||
/// application wrote the ambiguous credential.
|
/// application wrote the ambiguous credential.
|
||||||
pub async 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().await
|
self.inner.get_attributes().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,26 +358,22 @@ impl Entry {
|
||||||
/// cross-platform use, each credential store ignores (without error) any specified attributes
|
/// cross-platform use, each credential store ignores (without error) any specified attributes
|
||||||
/// that aren't supported by that store.
|
/// that aren't supported by that store.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](Error::NoEntry) error if there isn't a credential for this entry.
|
/// Returns a [`NoEntry`](Error::NoEntry) error if there isn't a credential for this entry.
|
||||||
///
|
///
|
||||||
/// Can return an [Ambiguous](Error::Ambiguous) error
|
/// Can return an [`Ambiguous`](Error::Ambiguous) error
|
||||||
/// if there is more than one platform credential
|
/// if there is more than one platform credential
|
||||||
/// that matches this entry. This can only happen
|
/// that matches this entry. This can only happen
|
||||||
/// on some platforms, and then only if a third-party
|
/// on some platforms, and then only if a third-party
|
||||||
/// application wrote the ambiguous credential.
|
/// application wrote the ambiguous credential.
|
||||||
pub async 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).await
|
self.inner.update_attributes(attributes).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the underlying credential for this entry.
|
/// Delete the underlying credential for this entry.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](Error::NoEntry) error if there isn't one.
|
/// Returns a [`NoEntry`](Error::NoEntry) error if there isn't one.
|
||||||
///
|
///
|
||||||
/// Can return an [Ambiguous](Error::Ambiguous) error
|
/// Can return an [`Ambiguous`](Error::Ambiguous) error
|
||||||
/// if there is more than one platform credential
|
/// if there is more than one platform credential
|
||||||
/// that matches this entry. This can only happen
|
/// that matches this entry. This can only happen
|
||||||
/// on some platforms, and then only if a third-party
|
/// on some platforms, and then only if a third-party
|
||||||
|
|
@ -409,7 +383,6 @@ impl Entry {
|
||||||
/// structure, which is controlled by Rust. It only
|
/// structure, which is controlled by Rust. It only
|
||||||
/// affects the underlying credential store.
|
/// affects the underlying credential store.
|
||||||
pub async fn delete_credential(&self) -> Result<()> {
|
pub async fn delete_credential(&self) -> Result<()> {
|
||||||
debug!("delete entry {:?}", self.inner);
|
|
||||||
self.inner.delete_credential().await
|
self.inner.delete_credential().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,9 +403,6 @@ doc_comment::doctest!("../README.md", readme);
|
||||||
/// There are no actual tests in this module.
|
/// There are no actual tests in this module.
|
||||||
/// Instead, it contains generics that each keystore invokes in their tests,
|
/// Instead, it contains generics that each keystore invokes in their tests,
|
||||||
/// passing their store-specific parameters for the generic ones.
|
/// passing their store-specific parameters for the generic ones.
|
||||||
//
|
|
||||||
// Since iOS doesn't use any of these generics, we allow dead code.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{Entry, Error, Result, credential::CredentialApi};
|
use super::{Entry, Error, Result, credential::CredentialApi};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -451,25 +421,6 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a platform-specific credential given the constructor, service, user, and attributes
|
|
||||||
pub(crate) fn entry_from_constructor_and_attributes<F, T>(
|
|
||||||
f: F,
|
|
||||||
service: &str,
|
|
||||||
user: &str,
|
|
||||||
attrs: &HashMap<&str, &str>,
|
|
||||||
) -> Entry
|
|
||||||
where
|
|
||||||
F: FnOnce(Option<&str>, &str, &str, &HashMap<&str, &str>) -> Result<T>,
|
|
||||||
T: 'static + CredentialApi + Send + Sync,
|
|
||||||
{
|
|
||||||
match f(None, service, user, attrs) {
|
|
||||||
Ok(credential) => Entry::new_with_credential(Box::new(credential)),
|
|
||||||
Err(err) => {
|
|
||||||
panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async 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
|
entry
|
||||||
.set_password(in_pass)
|
.set_password(in_pass)
|
||||||
|
|
@ -482,7 +433,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
in_pass, out_pass,
|
in_pass, out_pass,
|
||||||
"Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
|
"Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A basic round-trip unit test given an entry and a password.
|
/// A basic round-trip unit test given an entry and a password.
|
||||||
|
|
@ -555,7 +506,7 @@ mod tests {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
"Missing entry has password"
|
"Missing entry has password"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn test_empty_password<F>(f: F)
|
pub(crate) async fn test_empty_password<F>(f: F)
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,8 @@ All credentials on macOS are stored in secure stores called _keychains_.
|
||||||
The OS automatically creates three of them that live on filesystem,
|
The OS automatically creates three of them that live on filesystem,
|
||||||
called _User_ (aka login), _Common_, and _System_. In addition, removable
|
called _User_ (aka login), _Common_, and _System_. In addition, removable
|
||||||
media can contain a keychain which can be registered under the name _Dynamic_.
|
media can contain a keychain which can be registered under the name _Dynamic_.
|
||||||
Finally, on Apple Silicon devices, there is a more highly protected keychain
|
|
||||||
(called the _Data Protection_ or simply _Protected_ keychain). This is the same
|
|
||||||
keychain that is used by apps on iOS; so this module actually returns
|
|
||||||
iOS credentials for entries in the Data Protection keychain.
|
|
||||||
|
|
||||||
The target attribute of an [Entry](crate::Entry) determines (case-insensitive)
|
The target attribute of an [`Entry`](crate::Entry) determines (case-insensitive)
|
||||||
which keychain that entry's credential is created in or searched for.
|
which keychain that entry's credential is created in or searched for.
|
||||||
If the entry has no target, or the specified target doesn't name (case-insensitive)
|
If the entry has no target, or the specified target doesn't name (case-insensitive)
|
||||||
one of the keychains listed above, the 'User' keychain is used.
|
one of the keychains listed above, the 'User' keychain is used.
|
||||||
|
|
@ -80,7 +76,7 @@ impl CredentialApi for MacCredential {
|
||||||
|
|
||||||
/// Look up the password for this entry, if any.
|
/// Look up the password for this entry, if any.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
/// Returns a [`NoEntry`](ErrorCode::NoEntry) error if there is no
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async fn get_password(&self) -> Result<String> {
|
async fn get_password(&self) -> Result<String> {
|
||||||
let (password_bytes, _) =
|
let (password_bytes, _) =
|
||||||
|
|
@ -91,7 +87,7 @@ impl CredentialApi for MacCredential {
|
||||||
|
|
||||||
/// Look up the secret for this entry, if any.
|
/// Look up the secret for this entry, if any.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
/// Returns a [`NoEntry`](ErrorCode::NoEntry) error if there is no
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async fn get_secret(&self) -> Result<Vec<u8>> {
|
async fn get_secret(&self) -> Result<Vec<u8>> {
|
||||||
let (password_bytes, _) =
|
let (password_bytes, _) =
|
||||||
|
|
@ -102,7 +98,7 @@ impl CredentialApi for MacCredential {
|
||||||
|
|
||||||
/// Delete the underlying generic credential for this entry, if any.
|
/// Delete the underlying generic credential for this entry, if any.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
/// Returns a [`NoEntry`](ErrorCode::NoEntry) error if there is no
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async fn delete_credential(&self) -> Result<()> {
|
async fn delete_credential(&self) -> Result<()> {
|
||||||
let (_, item) =
|
let (_, item) =
|
||||||
|
|
@ -113,7 +109,7 @@ impl CredentialApi for MacCredential {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the underlying concrete object with an `Any` type so that it can
|
/// Return the underlying concrete object with an `Any` type so that it can
|
||||||
/// be downgraded to a [MacCredential] for platform-specific processing.
|
/// be downgraded to a [`MacCredential`] for platform-specific processing.
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +137,7 @@ impl MacCredential {
|
||||||
///
|
///
|
||||||
/// Creating a credential does not put anything into the keychain.
|
/// Creating a credential does not put anything into the keychain.
|
||||||
/// The keychain entry will be created
|
/// The keychain entry will be created
|
||||||
/// when [set_password](MacCredential::set_password) is
|
/// when [`set_password`](MacCredential::set_password) is
|
||||||
/// called.
|
/// called.
|
||||||
///
|
///
|
||||||
/// This will fail if the service or user strings are empty,
|
/// This will fail if the service or user strings are empty,
|
||||||
|
|
@ -178,7 +174,7 @@ impl MacCredential {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The builder for Mac keychain credentials
|
/// The builder for Mac keychain credentials
|
||||||
pub struct MacCredentialBuilder {}
|
pub struct MacCredentialBuilder;
|
||||||
|
|
||||||
/// Returns an instance of the Mac credential builder.
|
/// Returns an instance of the Mac credential builder.
|
||||||
///
|
///
|
||||||
|
|
@ -189,7 +185,7 @@ pub fn default_credential_builder() -> Box<CredentialBuilder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CredentialBuilderApi for MacCredentialBuilder {
|
impl CredentialBuilderApi for MacCredentialBuilder {
|
||||||
/// Build a [MacCredential] for the given target, service, and user.
|
/// Build a [`MacCredential`] for the given target, service, and user.
|
||||||
///
|
///
|
||||||
/// If a target is specified but not recognized as a keychain name,
|
/// If a target is specified but not recognized as a keychain name,
|
||||||
/// the User keychain is selected.
|
/// the User keychain is selected.
|
||||||
|
|
@ -207,7 +203,7 @@ impl CredentialBuilderApi for MacCredentialBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the underlying builder object with an `Any` type so that it can
|
/// Return the underlying builder object with an `Any` type so that it can
|
||||||
/// be downgraded to a [MacCredentialBuilder] for platform-specific processing.
|
/// be downgraded to a [`MacCredentialBuilder`] for platform-specific processing.
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -226,11 +222,11 @@ pub enum MacKeychainDomain {
|
||||||
impl std::fmt::Display for MacKeychainDomain {
|
impl std::fmt::Display for MacKeychainDomain {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
MacKeychainDomain::User => "User".fmt(f),
|
Self::User => "User".fmt(f),
|
||||||
MacKeychainDomain::System => "System".fmt(f),
|
Self::System => "System".fmt(f),
|
||||||
MacKeychainDomain::Common => "Common".fmt(f),
|
Self::Common => "Common".fmt(f),
|
||||||
MacKeychainDomain::Dynamic => "Dynamic".fmt(f),
|
Self::Dynamic => "Dynamic".fmt(f),
|
||||||
MacKeychainDomain::Protected => "Protected".fmt(f),
|
Self::Protected => "Protected".fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,12 +241,12 @@ impl std::str::FromStr for MacKeychainDomain {
|
||||||
/// or else we assume the login keychain is meant.
|
/// or else we assume the login keychain is meant.
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
match s.to_ascii_lowercase().as_str() {
|
match s.to_ascii_lowercase().as_str() {
|
||||||
"user" => Ok(MacKeychainDomain::User),
|
"user" => Ok(Self::User),
|
||||||
"system" => Ok(MacKeychainDomain::System),
|
"system" => Ok(Self::System),
|
||||||
"common" => Ok(MacKeychainDomain::Common),
|
"common" => Ok(Self::Common),
|
||||||
"dynamic" => Ok(MacKeychainDomain::Dynamic),
|
"dynamic" => Ok(Self::Dynamic),
|
||||||
"protected" => Ok(MacKeychainDomain::Protected),
|
"protected" => Ok(Self::Protected),
|
||||||
"data protection" => Ok(MacKeychainDomain::Protected),
|
"data protection" => Ok(Self::Protected),
|
||||||
_ => Err(ErrorCode::Invalid(
|
_ => Err(ErrorCode::Invalid(
|
||||||
"target".to_string(),
|
"target".to_string(),
|
||||||
format!("'{s}' is not User, System, Common, Dynamic, or Protected"),
|
format!("'{s}' is not User, System, Common, Dynamic, or Protected"),
|
||||||
|
|
@ -301,7 +297,7 @@ mod tests {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
default_credential_builder().persistence(),
|
default_credential_builder().persistence(),
|
||||||
CredentialPersistence::UntilDelete
|
CredentialPersistence::UntilDelete
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entry_new(service: &str, user: &str) -> Entry {
|
fn entry_new(service: &str, user: &str) -> Entry {
|
||||||
|
|
@ -399,7 +395,7 @@ mod tests {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(mac_cred.domain, super::MacKeychainDomain::User),
|
matches!(mac_cred.domain, super::MacKeychainDomain::User),
|
||||||
"wrong domain for unknown specifier"
|
"wrong domain for unknown specifier"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ keyring::set_default_credential_builder(keyring::mock::default_credential_builde
|
||||||
You can then create entries as you usually do, and call their usual methods
|
You can then create entries as you usually do, and call their usual methods
|
||||||
to set, get, and delete passwords. There is no persistence other than
|
to set, get, and delete passwords. There is no persistence other than
|
||||||
in the entry itself, so getting a password before setting it will always result
|
in the entry itself, so getting a password before setting it will always result
|
||||||
in a [NoEntry](Error::NoEntry) error.
|
in a [`NoEntry`](Error::NoEntry) error.
|
||||||
|
|
||||||
If you want a method call on an entry to fail in a specific way, you can
|
If you want a method call on an entry to fail in a specific way, you can
|
||||||
downcast the entry to a [MockCredential] and then call [set_error](MockCredential::set_error)
|
downcast the entry to a [`MockCredential`] and then call [`set_error`](MockCredential::set_error)
|
||||||
with the appropriate error. The next entry method called on the credential
|
with the appropriate error. The next entry method called on the credential
|
||||||
will fail with the error you set. The error will then be cleared, so the next
|
will fail with the error you set. The error will then be cleared, so the next
|
||||||
call on the mock will operate as usual. Here's a complete example:
|
call on the mock will operate as usual. Here's a complete example:
|
||||||
|
|
@ -53,7 +53,7 @@ pub struct MockCredential {
|
||||||
impl Default for MockCredential {
|
impl Default for MockCredential {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Mutex::new(RefCell::new(Default::default())),
|
inner: Mutex::new(RefCell::new(MockData::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,8 +187,8 @@ impl MockCredential {
|
||||||
///
|
///
|
||||||
/// Since mocks have no persistence between sessions,
|
/// Since mocks have no persistence between sessions,
|
||||||
/// new mocks always have no password.
|
/// new mocks always have no password.
|
||||||
fn new_with_target(_target: Option<&str>, _service: &str, _user: &str) -> Result<Self> {
|
fn new_with_target(_target: Option<&str>, _service: &str, _user: &str) -> Self {
|
||||||
Ok(Default::default())
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set an error to be returned from this mock credential.
|
/// Set an error to be returned from this mock credential.
|
||||||
|
|
@ -207,7 +207,7 @@ impl MockCredential {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The builder for mock credentials.
|
/// The builder for mock credentials.
|
||||||
pub struct MockCredentialBuilder {}
|
pub struct MockCredentialBuilder;
|
||||||
|
|
||||||
impl CredentialBuilderApi for MockCredentialBuilder {
|
impl CredentialBuilderApi for MockCredentialBuilder {
|
||||||
/// Build a mock credential for the given target, service, and user.
|
/// Build a mock credential for the given target, service, and user.
|
||||||
|
|
@ -215,7 +215,7 @@ impl CredentialBuilderApi for MockCredentialBuilder {
|
||||||
/// Since mocks don't persist between sessions, all mocks
|
/// Since mocks don't persist between sessions, all mocks
|
||||||
/// start off without passwords.
|
/// start off without passwords.
|
||||||
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
|
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
|
||||||
let credential = MockCredential::new_with_target(target, service, user)?;
|
let credential = MockCredential::new_with_target(target, service, user);
|
||||||
Ok(Box::new(credential))
|
Ok(Box::new(credential))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,11 +246,11 @@ mod tests {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
default_credential_builder().persistence(),
|
default_credential_builder().persistence(),
|
||||||
CredentialPersistence::EntryOnly
|
CredentialPersistence::EntryOnly
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entry_new(service: &str, user: &str) -> Entry {
|
fn entry_new(service: &str, user: &str) -> Entry {
|
||||||
let credential = MockCredential::new_with_target(None, service, user).unwrap();
|
let credential = MockCredential::new_with_target(None, service, user);
|
||||||
Entry::new_with_credential(Box::new(credential))
|
Entry::new_with_credential(Box::new(credential))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,6 +336,6 @@ mod tests {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
"Able to read a deleted ascii password"
|
"Able to read a deleted ascii password"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ use crate::error::{Error as ErrorCode, Result, decode_password};
|
||||||
/// The representation of an item in the secret-service.
|
/// The representation of an item in the secret-service.
|
||||||
///
|
///
|
||||||
/// This structure has two roles. On the one hand, it captures all the
|
/// This structure has two roles. On the one hand, it captures all the
|
||||||
/// information a user specifies for an [Entry](crate::Entry)
|
/// information a user specifies for an [`Entry`](crate::Entry)
|
||||||
/// and so is the basis for our search
|
/// and so is the basis for our search
|
||||||
/// (or creation) of an item for that entry. On the other hand, when
|
/// (or creation) of an item for that entry. On the other hand, when
|
||||||
/// a search is ambiguous, each item found is represented by a credential that
|
/// a search is ambiguous, each item found is represented by a credential that
|
||||||
|
|
@ -104,7 +104,7 @@ pub struct SsCredential {
|
||||||
impl CredentialApi for SsCredential {
|
impl CredentialApi for SsCredential {
|
||||||
/// Sets the password on a unique matching item, if it exists, or creates one if necessary.
|
/// 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
|
/// If there are multiple matches, returns an [`Ambiguous`](ErrorCode::Ambiguous) error with
|
||||||
/// a credential for each matching item.
|
/// a credential for each matching item.
|
||||||
///
|
///
|
||||||
/// When creating, the item is put into a collection named by the credential's `target`
|
/// When creating, the item is put into a collection named by the credential's `target`
|
||||||
|
|
@ -115,7 +115,7 @@ impl CredentialApi for SsCredential {
|
||||||
|
|
||||||
/// Sets the secret on a unique matching item, if it exists, or creates one if necessary.
|
/// 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
|
/// If there are multiple matches, returns an [`Ambiguous`](ErrorCode::Ambiguous) error
|
||||||
/// with a credential for each matching item.
|
/// with a credential for each matching item.
|
||||||
///
|
///
|
||||||
/// When creating, the item is put into a collection named by the credential's `target`
|
/// When creating, the item is put into a collection named by the credential's `target`
|
||||||
|
|
@ -127,7 +127,7 @@ impl CredentialApi for SsCredential {
|
||||||
.map_err(platform_failure)?;
|
.map_err(platform_failure)?;
|
||||||
match self.matching_items(&ss).await {
|
match self.matching_items(&ss).await {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
for item in items.iter() {
|
for item in &items {
|
||||||
set_item_secret(item, secret).await?;
|
set_item_secret(item, secret).await?;
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -137,7 +137,7 @@ impl CredentialApi for SsCredential {
|
||||||
}
|
}
|
||||||
// if there is no existing item, create one for this credential. In order to create
|
// if there is no existing item, create one for this credential. In order to create
|
||||||
// an item, the credential must have an explicit target. All entries created with
|
// 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
|
// 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.
|
// created to wrap 3rd-party items that don't have `target` attributes may not.
|
||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
.await
|
.await
|
||||||
|
|
@ -162,8 +162,8 @@ impl CredentialApi for SsCredential {
|
||||||
|
|
||||||
/// Gets the password on a unique matching item, if it exists.
|
/// 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 no matching items, returns a [`NoEntry`](ErrorCode::NoEntry) error.
|
||||||
/// If there are multiple matches, returns an [Ambiguous](ErrorCode::Ambiguous)
|
/// If there are multiple matches, returns an [`Ambiguous`](ErrorCode::Ambiguous)
|
||||||
/// error with a credential for each matching item.
|
/// error with a credential for each matching item.
|
||||||
async fn get_password(&self) -> Result<String> {
|
async fn get_password(&self) -> Result<String> {
|
||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
|
|
@ -183,9 +183,9 @@ impl CredentialApi for SsCredential {
|
||||||
/// Gets the secret on a unique matching item, if it exists.
|
/// Gets the secret on a unique matching item, if it exists.
|
||||||
///
|
///
|
||||||
/// If there are no
|
/// If there are no
|
||||||
/// matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
|
/// matching items, returns a [`NoEntry`](ErrorCode::NoEntry) error.
|
||||||
/// If there are multiple matches,
|
/// If there are multiple matches,
|
||||||
/// returns an [Ambiguous](ErrorCode::Ambiguous)
|
/// returns an [`Ambiguous`](ErrorCode::Ambiguous)
|
||||||
/// error with a credential for each matching item.
|
/// error with a credential for each matching item.
|
||||||
async fn get_secret(&self) -> Result<Vec<u8>> {
|
async fn get_secret(&self) -> Result<Vec<u8>> {
|
||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
|
|
@ -236,8 +236,8 @@ impl CredentialApi for SsCredential {
|
||||||
|
|
||||||
/// Deletes the unique matching item, if it exists.
|
/// Deletes the unique matching item, if it exists.
|
||||||
///
|
///
|
||||||
/// If there are no matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
|
/// 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 multiple matches, returns an [`Ambiguous`](ErrorCode::Ambiguous)
|
||||||
/// error with a credential for each matching item.
|
/// error with a credential for each matching item.
|
||||||
async fn delete_credential(&self) -> Result<()> {
|
async fn delete_credential(&self) -> Result<()> {
|
||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
|
|
@ -260,7 +260,7 @@ impl CredentialApi for SsCredential {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expose the concrete debug formatter for use via the [Credential] trait
|
/// Expose the concrete debug formatter for use via the [`Credential`] trait
|
||||||
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
std::fmt::Debug::fmt(self, f)
|
std::fmt::Debug::fmt(self, f)
|
||||||
}
|
}
|
||||||
|
|
@ -273,7 +273,7 @@ impl SsCredential {
|
||||||
///
|
///
|
||||||
/// Creating this credential does not create a matching item.
|
/// Creating this credential does not create a matching item.
|
||||||
/// If there isn't already one there, it will be created only
|
/// If there isn't already one there, it will be created only
|
||||||
/// when [set_password](SsCredential::set_password) is
|
/// when [`set_password`](SsCredential::set_password) is
|
||||||
/// called.
|
/// called.
|
||||||
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
|
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
|
||||||
if let Some("") = target {
|
if let Some("") = target {
|
||||||
|
|
@ -350,8 +350,8 @@ impl SsCredential {
|
||||||
|
|
||||||
/// If there are multiple matching items for this credential, get all of their passwords.
|
/// If there are multiple matching items for this credential, get all of their passwords.
|
||||||
///
|
///
|
||||||
/// (This is useful if [get_password](SsCredential::get_password)
|
/// (This is useful if [`get_password`](SsCredential::get_password)
|
||||||
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
|
/// returns an [`Ambiguous`](ErrorCode::Ambiguous) error.)
|
||||||
pub async fn get_all_passwords(&self) -> Result<Vec<String>> {
|
pub async fn get_all_passwords(&self) -> Result<Vec<String>> {
|
||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
.await
|
.await
|
||||||
|
|
@ -359,7 +359,7 @@ impl SsCredential {
|
||||||
match self.matching_items(&ss).await {
|
match self.matching_items(&ss).await {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
let mut passwords = Vec::new();
|
let mut passwords = Vec::new();
|
||||||
for item in items.iter() {
|
for item in &items {
|
||||||
passwords.push(get_item_password(item).await?);
|
passwords.push(get_item_password(item).await?);
|
||||||
}
|
}
|
||||||
Ok(passwords)
|
Ok(passwords)
|
||||||
|
|
@ -368,7 +368,7 @@ impl SsCredential {
|
||||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||||
let items = self.find_legacy_items(&collection).await?;
|
let items = self.find_legacy_items(&collection).await?;
|
||||||
let mut passwords = Vec::new();
|
let mut passwords = Vec::new();
|
||||||
for item in items.iter() {
|
for item in &items {
|
||||||
passwords.push(get_item_password(item).await?);
|
passwords.push(get_item_password(item).await?);
|
||||||
}
|
}
|
||||||
Ok(passwords)
|
Ok(passwords)
|
||||||
|
|
@ -379,15 +379,15 @@ impl SsCredential {
|
||||||
|
|
||||||
/// If there are multiple matching items for this credential, delete all of them.
|
/// If there are multiple matching items for this credential, delete all of them.
|
||||||
///
|
///
|
||||||
/// (This is useful if [delete_credential](SsCredential::delete_credential)
|
/// (This is useful if [`delete_credential`](SsCredential::delete_credential)
|
||||||
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
|
/// returns an [`Ambiguous`](ErrorCode::Ambiguous) error.)
|
||||||
pub async fn delete_all_passwords(&self) -> Result<()> {
|
pub async fn delete_all_passwords(&self) -> Result<()> {
|
||||||
let ss = SecretService::connect(EncryptionType::Dh)
|
let ss = SecretService::connect(EncryptionType::Dh)
|
||||||
.await
|
.await
|
||||||
.map_err(platform_failure)?;
|
.map_err(platform_failure)?;
|
||||||
match self.matching_items(&ss).await {
|
match self.matching_items(&ss).await {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
for item in items.iter() {
|
for item in &items {
|
||||||
delete_item(item).await?;
|
delete_item(item).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -395,7 +395,7 @@ impl SsCredential {
|
||||||
Err(ErrorCode::NoEntry) => {
|
Err(ErrorCode::NoEntry) => {
|
||||||
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
let collection = ss.get_default_collection().await.map_err(decode_error)?;
|
||||||
let items = self.find_legacy_items(&collection).await?;
|
let items = self.find_legacy_items(&collection).await?;
|
||||||
for item in items.iter() {
|
for item in &items {
|
||||||
delete_item(item).await?;
|
delete_item(item).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -428,8 +428,8 @@ impl SsCredential {
|
||||||
|
|
||||||
/// Find and unlock a unique item matching this credential.
|
/// 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 no matching items, then a [`NoEntry`](ErrorCode::NoEntry) error is returned.
|
||||||
/// If there are multiple matches, then an [Ambiguous](ErrorCode::Ambiguous) error is
|
/// 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.
|
/// 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>> {
|
async fn unique_matching_item<'a>(&self, ss: &'a SecretService<'_>) -> Result<Item<'a>> {
|
||||||
let mut items = self.matching_items(ss).await?;
|
let mut items = self.matching_items(ss).await?;
|
||||||
|
|
@ -443,9 +443,9 @@ impl SsCredential {
|
||||||
let search = ss.search_items(attributes).await.map_err(decode_error)?;
|
let search = ss.search_items(attributes).await.map_err(decode_error)?;
|
||||||
for item in search.locked.iter().chain(search.unlocked.iter()) {
|
for item in search.locked.iter().chain(search.unlocked.iter()) {
|
||||||
let cred = Self::new_from_item(item).await?;
|
let cred = Self::new_from_item(item).await?;
|
||||||
creds.push(Box::new(cred))
|
creds.push(Box::new(cred));
|
||||||
}
|
}
|
||||||
return Err(ErrorCode::Ambiguous(creds));
|
Err(ErrorCode::Ambiguous(creds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -474,7 +474,7 @@ impl SsCredential {
|
||||||
/// That preserves the legacy behavior at the cost of a second round-trip through
|
/// That preserves the legacy behavior at the cost of a second round-trip through
|
||||||
/// the secret service for the collection search.
|
/// the secret service for the collection search.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there are no matching items or
|
/// 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 the credential being searched does not have the default target.
|
||||||
async fn find_legacy_items<'a>(&self, collection: &'a Collection<'_>) -> Result<Vec<Item<'a>>> {
|
async fn find_legacy_items<'a>(&self, collection: &'a Collection<'_>) -> Result<Vec<Item<'a>>> {
|
||||||
if let Some("default") = self.target.as_deref() {
|
if let Some("default") = self.target.as_deref() {
|
||||||
|
|
@ -496,9 +496,9 @@ impl SsCredential {
|
||||||
/// Find unique legacy item in the given collection if the credential being searched has
|
/// Find unique legacy item in the given collection if the credential being searched has
|
||||||
/// the default target.
|
/// the default target.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there are no matching items or
|
/// 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 the credential being searched does not have the default target.
|
||||||
/// If there are multiple matches, then an [Ambiguous](ErrorCode::Ambiguous) error is
|
/// 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.
|
/// returned with a vector containing one credential for each of the matching items.
|
||||||
async fn find_unique_legacy_item<'a>(
|
async fn find_unique_legacy_item<'a>(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -515,11 +515,11 @@ impl SsCredential {
|
||||||
.search_items(attributes)
|
.search_items(attributes)
|
||||||
.await
|
.await
|
||||||
.map_err(decode_error)?;
|
.map_err(decode_error)?;
|
||||||
for item in search.iter() {
|
for item in &search {
|
||||||
let cred = Self::new_from_item(item).await?;
|
let cred = Self::new_from_item(item).await?;
|
||||||
creds.push(Box::new(cred))
|
creds.push(Box::new(cred));
|
||||||
}
|
}
|
||||||
return Err(ErrorCode::Ambiguous(creds));
|
Err(ErrorCode::Ambiguous(creds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -535,7 +535,7 @@ impl SsCredential {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Similar to [all_attributes](SsCredential::all_attributes),
|
/// Similar to [`all_attributes`](SsCredential::all_attributes),
|
||||||
/// but this just selects the ones we search on
|
/// but this just selects the ones we search on
|
||||||
fn search_attributes(&self, omit_target: bool) -> HashMap<&str, &str> {
|
fn search_attributes(&self, omit_target: bool) -> HashMap<&str, &str> {
|
||||||
let mut result: HashMap<&str, &str> = HashMap::new();
|
let mut result: HashMap<&str, &str> = HashMap::new();
|
||||||
|
|
@ -550,7 +550,7 @@ impl SsCredential {
|
||||||
|
|
||||||
/// The builder for secret-service credentials
|
/// The builder for secret-service credentials
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct SsCredentialBuilder {}
|
pub struct SsCredentialBuilder;
|
||||||
|
|
||||||
/// Returns an instance of the secret-service credential builder.
|
/// Returns an instance of the secret-service credential builder.
|
||||||
///
|
///
|
||||||
|
|
@ -561,7 +561,7 @@ pub fn default_credential_builder() -> Box<CredentialBuilder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CredentialBuilderApi for SsCredentialBuilder {
|
impl CredentialBuilderApi for SsCredentialBuilder {
|
||||||
/// Build an [SsCredential] for the given target, service, and user.
|
/// Build an [`SsCredential`] for the given target, service, and user.
|
||||||
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
|
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
|
||||||
Ok(Box::new(SsCredential::new_with_target(
|
Ok(Box::new(SsCredential::new_with_target(
|
||||||
target, service, user,
|
target, service, user,
|
||||||
|
|
@ -569,7 +569,7 @@ impl CredentialBuilderApi for SsCredentialBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the underlying builder object with an `Any` type so that it can
|
/// Return the underlying builder object with an `Any` type so that it can
|
||||||
/// be downgraded to an [SsCredentialBuilder] for platform-specific processing.
|
/// be downgraded to an [`SsCredentialBuilder`] for platform-specific processing.
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -660,10 +660,10 @@ pub async fn update_item_attributes(
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let existing = item.get_attributes().await.map_err(decode_error)?;
|
let existing = item.get_attributes().await.map_err(decode_error)?;
|
||||||
let mut updated: HashMap<&str, &str> = HashMap::new();
|
let mut updated: HashMap<&str, &str> = HashMap::new();
|
||||||
for (k, v) in existing.iter() {
|
for (k, v) in &existing {
|
||||||
updated.insert(k, v);
|
updated.insert(k, v);
|
||||||
}
|
}
|
||||||
for (k, v) in attributes.iter() {
|
for (k, v) in attributes {
|
||||||
if k.eq(&"target") || k.eq(&"service") || k.eq(&"username") {
|
if k.eq(&"target") || k.eq(&"service") || k.eq(&"username") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -735,7 +735,7 @@ mod tests {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
default_credential_builder().persistence(),
|
default_credential_builder().persistence(),
|
||||||
CredentialPersistence::UntilDelete
|
CredentialPersistence::UntilDelete
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entry_new(service: &str, user: &str) -> Entry {
|
fn entry_new(service: &str, user: &str) -> Entry {
|
||||||
|
|
@ -803,7 +803,7 @@ mod tests {
|
||||||
actual.attributes.get(key).expect("Missing attribute"),
|
actual.attributes.get(key).expect("Missing attribute"),
|
||||||
value,
|
value,
|
||||||
"Attribute mismatch"
|
"Attribute mismatch"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
entry
|
entry
|
||||||
.delete_credential()
|
.delete_credential()
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ So if you have a custom algorithm you want to use for computing the Windows targ
|
||||||
you can specify the target name directly. (You still need to provide a service and username,
|
you can specify the target name directly. (You still need to provide a service and username,
|
||||||
because they are used in the credential's metadata.)
|
because they are used in the credential's metadata.)
|
||||||
|
|
||||||
The [get_attributes](crate::Entry::get_attributes)
|
The [`get_attributes`](crate::Entry::get_attributes)
|
||||||
call will return the values in the `username`, `comment`, and `target_alias` fields
|
call will return the values in the `username`, `comment`, and `target_alias` fields
|
||||||
(using those strings as the attribute names),
|
(using those strings as the attribute names),
|
||||||
and the [update_attributes](crate::Entry::update_attributes)
|
and the [`update_attributes`](crate::Entry::update_attributes)
|
||||||
call allows setting those fields.
|
call allows setting those fields.
|
||||||
|
|
||||||
## Caveat
|
## Caveat
|
||||||
|
|
@ -36,6 +36,8 @@ shown that modifying the same entry in the same (almost simultaneous) order from
|
||||||
different threads produces different results on different runs.
|
different threads produces different results on different runs.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
|
use crate::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
|
||||||
use crate::error::{Error as ErrorCode, Result};
|
use crate::error::{Error as ErrorCode, Result};
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
|
|
@ -71,6 +73,7 @@ pub struct WinCredential {
|
||||||
// BOOL is i32 (false = 0, true = 1)
|
// BOOL is i32 (false = 0, true = 1)
|
||||||
// PCREDENTIALW = *mut CREDENTIALW
|
// PCREDENTIALW = *mut CREDENTIALW
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
impl CredentialApi for WinCredential {
|
impl CredentialApi for WinCredential {
|
||||||
/// Create and write a credential with password for this entry.
|
/// Create and write a credential with password for this entry.
|
||||||
///
|
///
|
||||||
|
|
@ -86,7 +89,7 @@ impl CredentialApi for WinCredential {
|
||||||
let mut blob_u16 = to_wstr_no_null(password);
|
let mut blob_u16 = to_wstr_no_null(password);
|
||||||
let mut blob = vec![0; blob_u16.len() * 2];
|
let mut blob = vec![0; blob_u16.len() * 2];
|
||||||
LittleEndian::write_u16_into(&blob_u16, &mut blob);
|
LittleEndian::write_u16_into(&blob_u16, &mut blob);
|
||||||
let result = self.set_secret(&blob);
|
let result = self.set_secret(&blob).await;
|
||||||
// make sure that the copies of the secret are erased
|
// make sure that the copies of the secret are erased
|
||||||
blob_u16.zeroize();
|
blob_u16.zeroize();
|
||||||
blob.zeroize();
|
blob.zeroize();
|
||||||
|
|
@ -105,7 +108,7 @@ impl CredentialApi for WinCredential {
|
||||||
|
|
||||||
/// Look up the password for this entry, if any.
|
/// Look up the password for this entry, if any.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
/// Returns a [`NoEntry`](ErrorCode::NoEntry) error if there is no
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async fn get_password(&self) -> Result<String> {
|
async fn get_password(&self) -> Result<String> {
|
||||||
self.extract_from_platform(extract_password)
|
self.extract_from_platform(extract_password)
|
||||||
|
|
@ -113,7 +116,7 @@ impl CredentialApi for WinCredential {
|
||||||
|
|
||||||
/// Look up the secret for this entry, if any.
|
/// Look up the secret for this entry, if any.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
/// Returns a [`NoEntry`](ErrorCode::NoEntry) error if there is no
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async fn get_secret(&self) -> Result<Vec<u8>> {
|
async fn get_secret(&self) -> Result<Vec<u8>> {
|
||||||
self.extract_from_platform(extract_secret)
|
self.extract_from_platform(extract_secret)
|
||||||
|
|
@ -121,7 +124,7 @@ impl CredentialApi for WinCredential {
|
||||||
|
|
||||||
/// Get the attributes from the credential for this entry, if it exists.
|
/// Get the attributes from the credential for this entry, if it exists.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
/// Returns a [`NoEntry`](ErrorCode::NoEntry) error if there is no
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async 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 cred = self.extract_from_platform(Self::extract_credential)?;
|
||||||
|
|
@ -134,19 +137,19 @@ impl CredentialApi for WinCredential {
|
||||||
|
|
||||||
/// Update the attributes on the credential for this entry, if it exists.
|
/// Update the attributes on the credential for this entry, if it exists.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
/// Returns a [`NoEntry`](ErrorCode::NoEntry) error if there is no
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async 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 secret = self.extract_from_platform(extract_secret)?;
|
||||||
let mut cred = self.extract_from_platform(Self::extract_credential)?;
|
let mut cred = self.extract_from_platform(Self::extract_credential)?;
|
||||||
if let Some(comment) = attributes.get(&"comment") {
|
if let Some(comment) = attributes.get(&"comment") {
|
||||||
cred.comment = comment.to_string();
|
cred.comment = (*comment).to_string();
|
||||||
}
|
}
|
||||||
if let Some(target_alias) = attributes.get(&"target_alias") {
|
if let Some(target_alias) = attributes.get(&"target_alias") {
|
||||||
cred.target_alias = target_alias.to_string();
|
cred.target_alias = (*target_alias).to_string();
|
||||||
}
|
}
|
||||||
if let Some(username) = attributes.get(&"username") {
|
if let Some(username) = attributes.get(&"username") {
|
||||||
cred.username = username.to_string();
|
cred.username = (*username).to_string();
|
||||||
}
|
}
|
||||||
cred.validate_attributes(Some(&secret), None)?;
|
cred.validate_attributes(Some(&secret), None)?;
|
||||||
cred.save_credential(&secret)
|
cred.save_credential(&secret)
|
||||||
|
|
@ -154,7 +157,7 @@ impl CredentialApi for WinCredential {
|
||||||
|
|
||||||
/// Delete the underlying generic credential for this entry, if any.
|
/// Delete the underlying generic credential for this entry, if any.
|
||||||
///
|
///
|
||||||
/// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no
|
/// Returns a [`NoEntry`](ErrorCode::NoEntry) error if there is no
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async fn delete_credential(&self) -> Result<()> {
|
async fn delete_credential(&self) -> Result<()> {
|
||||||
self.validate_attributes(None, None)?;
|
self.validate_attributes(None, None)?;
|
||||||
|
|
@ -167,12 +170,12 @@ impl CredentialApi for WinCredential {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the underlying concrete object with an `Any` type so that it can
|
/// Return the underlying concrete object with an `Any` type so that it can
|
||||||
/// be downgraded to a [WinCredential] for platform-specific processing.
|
/// be downgraded to a [`WinCredential`] for platform-specific processing.
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expose the concrete debug formatter for use via the [Credential] trait
|
/// Expose the concrete debug formatter for use via the [`Credential`] trait
|
||||||
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
std::fmt::Debug::fmt(self, f)
|
std::fmt::Debug::fmt(self, f)
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +237,7 @@ impl WinCredential {
|
||||||
/// Write this credential into the underlying store as a Generic credential
|
/// Write this credential into the underlying store as a Generic credential
|
||||||
///
|
///
|
||||||
/// You must always have validated attributes before you call this!
|
/// You must always have validated attributes before you call this!
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
fn save_credential(&self, secret: &[u8]) -> Result<()> {
|
fn save_credential(&self, secret: &[u8]) -> Result<()> {
|
||||||
let mut username = to_wstr(&self.username);
|
let mut username = to_wstr(&self.username);
|
||||||
let mut target_name = to_wstr(&self.target_name);
|
let mut target_name = to_wstr(&self.target_name);
|
||||||
|
|
@ -304,28 +308,26 @@ impl WinCredential {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match result {
|
if result == 0 {
|
||||||
0 => {
|
// `CredReadW` failed, so no allocation has been done, so no free needs to be done
|
||||||
// `CredReadW` failed, so no allocation has been done, so no free needs to be done
|
Err(decode_error())
|
||||||
Err(decode_error())
|
} else {
|
||||||
}
|
// `CredReadW` succeeded, so p_credential points at an allocated credential.
|
||||||
_ => {
|
// To do anything with it, we need to cast it to the right type. That takes two steps:
|
||||||
// `CredReadW` succeeded, so p_credential points at an allocated credential.
|
// first we remove the "uninitialized" guard around it, then we reinterpret it as a
|
||||||
// To do anything with it, we need to cast it to the right type. That takes two steps:
|
// pointer to the right structure type.
|
||||||
// first we remove the "uninitialized" guard around it, then we reinterpret it as a
|
let p_credential = unsafe { p_credential.assume_init() };
|
||||||
// pointer to the right structure type.
|
let w_credential: CREDENTIALW = unsafe { *p_credential };
|
||||||
let p_credential = unsafe { p_credential.assume_init() };
|
// Now we can apply the passed extractor function to the credential.
|
||||||
let w_credential: CREDENTIALW = unsafe { *p_credential };
|
let result = f(&w_credential);
|
||||||
// Now we can apply the passed extractor function to the credential.
|
// Finally, we erase the secret and free the allocated credential.
|
||||||
let result = f(&w_credential);
|
erase_secret(&w_credential);
|
||||||
// Finally, we erase the secret and free the allocated credential.
|
unsafe { CredFree(p_credential as *mut _) };
|
||||||
erase_secret(&w_credential);
|
result
|
||||||
unsafe { CredFree(p_credential as *mut _) };
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
fn extract_credential(w_credential: &CREDENTIALW) -> Result<Self> {
|
fn extract_credential(w_credential: &CREDENTIALW) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
username: unsafe { from_wstr(w_credential.UserName) },
|
username: unsafe { from_wstr(w_credential.UserName) },
|
||||||
|
|
@ -340,21 +342,11 @@ impl WinCredential {
|
||||||
/// Creating a credential does not create a matching Generic credential
|
/// Creating a credential does not create a matching Generic credential
|
||||||
/// in the Windows Credential Manager.
|
/// in the Windows Credential Manager.
|
||||||
/// If there isn't already one there, it will be created only
|
/// If there isn't already one there, it will be created only
|
||||||
/// when [set_password](WinCredential::set_password) is
|
/// when [`set_password`](WinCredential::set_password) is
|
||||||
/// called.
|
/// called.
|
||||||
pub fn new_with_target(
|
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
|
||||||
target: Option<&str>,
|
|
||||||
service: &str,
|
|
||||||
user: &str,
|
|
||||||
) -> Result<WinCredential> {
|
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
let credential = if let Some(target) = target {
|
let credential = if let Some(target) = target {
|
||||||
// if target.is_empty() {
|
|
||||||
// return Err(ErrorCode::Invalid(
|
|
||||||
// "target".to_string(),
|
|
||||||
// "cannot be empty".to_string(),
|
|
||||||
// ));
|
|
||||||
// }
|
|
||||||
Self {
|
Self {
|
||||||
// On Windows, the target name is all that's used to
|
// On Windows, the target name is all that's used to
|
||||||
// search for the credential, so we allow clients to
|
// search for the credential, so we allow clients to
|
||||||
|
|
@ -386,7 +378,7 @@ impl WinCredential {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The builder for Windows Generic credentials.
|
/// The builder for Windows Generic credentials.
|
||||||
pub struct WinCredentialBuilder {}
|
pub struct WinCredentialBuilder;
|
||||||
|
|
||||||
/// Returns an instance of the Windows credential builder.
|
/// Returns an instance of the Windows credential builder.
|
||||||
///
|
///
|
||||||
|
|
@ -397,7 +389,7 @@ pub fn default_credential_builder() -> Box<CredentialBuilder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CredentialBuilderApi for WinCredentialBuilder {
|
impl CredentialBuilderApi for WinCredentialBuilder {
|
||||||
/// Build a [WinCredential] for the given target, service, and user.
|
/// Build a [`WinCredential`] for the given target, service, and user.
|
||||||
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
|
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
|
||||||
Ok(Box::new(WinCredential::new_with_target(
|
Ok(Box::new(WinCredential::new_with_target(
|
||||||
target, service, user,
|
target, service, user,
|
||||||
|
|
@ -405,7 +397,7 @@ impl CredentialBuilderApi for WinCredentialBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the underlying builder object with an `Any` type so that it can
|
/// Return the underlying builder object with an `Any` type so that it can
|
||||||
/// be downgraded to a [WinCredentialBuilder] for platform-specific processing.
|
/// be downgraded to a [`WinCredentialBuilder`] for platform-specific processing.
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -435,6 +427,7 @@ fn extract_password(credential: &CREDENTIALW) -> Result<String> {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
fn extract_secret(credential: &CREDENTIALW) -> Result<Vec<u8>> {
|
fn extract_secret(credential: &CREDENTIALW) -> Result<Vec<u8>> {
|
||||||
let blob_pointer: *const u8 = credential.CredentialBlob;
|
let blob_pointer: *const u8 = credential.CredentialBlob;
|
||||||
let blob_len: usize = credential.CredentialBlobSize as usize;
|
let blob_len: usize = credential.CredentialBlobSize as usize;
|
||||||
|
|
@ -463,6 +456,7 @@ fn to_wstr_no_null(s: &str) -> Vec<u16> {
|
||||||
s.encode_utf16().collect()
|
s.encode_utf16().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::maybe_infinite_iter)]
|
||||||
unsafe fn from_wstr(ws: *const u16) -> String {
|
unsafe fn from_wstr(ws: *const u16) -> String {
|
||||||
// null pointer case, return empty string
|
// null pointer case, return empty string
|
||||||
if ws.is_null() {
|
if ws.is_null() {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ async fn test_missing_entry() {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
"Missing entry has password"
|
"Missing entry has password"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -42,7 +42,7 @@ async fn test_empty_password() {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
"Able to read a deleted password"
|
"Able to read a deleted password"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -71,7 +71,7 @@ async fn test_round_trip_ascii_password() {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
"Able to read a deleted ascii password"
|
"Able to read a deleted ascii password"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -100,7 +100,7 @@ async fn test_round_trip_non_ascii_password() {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
"Able to read a deleted non-ascii password"
|
"Able to read a deleted non-ascii password"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -127,7 +127,7 @@ async fn test_round_trip_random_secret() {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
"Able to read a deleted random secret"
|
"Able to read a deleted random secret"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -169,5 +169,5 @@ async fn test_update() {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
"Able to read a deleted updated password"
|
"Able to read a deleted updated password"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![cfg(feature = "keyring-tests")]
|
||||||
|
|
||||||
use common::{generate_random_string, init_logger};
|
use common::{generate_random_string, init_logger};
|
||||||
use uv_keyring::{Entry, Error};
|
use uv_keyring::{Entry, Error};
|
||||||
|
|
||||||
|
|
@ -9,50 +11,45 @@ async fn test_create_then_move() {
|
||||||
|
|
||||||
let name = generate_random_string();
|
let name = generate_random_string();
|
||||||
let entry = Entry::new(&name, &name).unwrap();
|
let entry = Entry::new(&name, &name).unwrap();
|
||||||
let test = move || {
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
let handle = tokio::spawn(async move {
|
||||||
.enable_all()
|
let password = "test ascii password";
|
||||||
.build()
|
entry
|
||||||
.unwrap();
|
.set_password(password)
|
||||||
runtime.block_on(async move {
|
.await
|
||||||
let password = "test ascii password";
|
.expect("Can't set initial ascii password");
|
||||||
entry
|
let stored_password = entry
|
||||||
.set_password(password)
|
.get_password()
|
||||||
.await
|
.await
|
||||||
.expect("Can't set initial ascii password");
|
.expect("Can't get ascii password");
|
||||||
let stored_password = entry
|
assert_eq!(
|
||||||
.get_password()
|
stored_password, password,
|
||||||
.await
|
"Retrieved and set initial ascii passwords don't match"
|
||||||
.expect("Can't get ascii password");
|
);
|
||||||
assert_eq!(
|
let password = "このきれいな花は桜です";
|
||||||
stored_password, password,
|
entry
|
||||||
"Retrieved and set initial ascii passwords don't match"
|
.set_password(password)
|
||||||
);
|
.await
|
||||||
let password = "このきれいな花は桜です";
|
.expect("Can't set non-ascii password");
|
||||||
entry
|
let stored_password = entry
|
||||||
.set_password(password)
|
.get_password()
|
||||||
.await
|
.await
|
||||||
.expect("Can't set non-ascii password");
|
.expect("Can't get non-ascii password");
|
||||||
let stored_password = entry
|
assert_eq!(
|
||||||
.get_password()
|
stored_password, password,
|
||||||
.await
|
"Retrieved and set non-ascii passwords don't match"
|
||||||
.expect("Can't get non-ascii password");
|
);
|
||||||
assert_eq!(
|
entry
|
||||||
stored_password, password,
|
.delete_credential()
|
||||||
"Retrieved and set non-ascii passwords don't match"
|
.await
|
||||||
);
|
.expect("Can't delete non-ascii password");
|
||||||
entry
|
assert!(
|
||||||
.delete_credential()
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
.await
|
"Able to read a deleted non-ascii password"
|
||||||
.expect("Can't delete non-ascii password");
|
);
|
||||||
assert!(
|
});
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
|
||||||
"Able to read a deleted non-ascii password"
|
handle.await.expect("Task failed");
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let handle = std::thread::spawn(test);
|
|
||||||
assert!(handle.join().is_ok(), "Couldn't execute on thread")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -63,38 +60,34 @@ async fn test_simultaneous_create_then_move() {
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let name = format!("{}-{}", generate_random_string(), i);
|
let name = format!("{}-{}", generate_random_string(), i);
|
||||||
let entry = Entry::new(&name, &name).expect("Can't create entry");
|
let entry = Entry::new(&name, &name).expect("Can't create entry");
|
||||||
let test = move || {
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
let handle = tokio::spawn(async move {
|
||||||
.enable_all()
|
entry
|
||||||
.build()
|
.set_password(&name)
|
||||||
.unwrap();
|
.await
|
||||||
runtime.block_on(async move {
|
.expect("Can't set ascii password");
|
||||||
entry
|
let stored_password = entry
|
||||||
.set_password(&name)
|
.get_password()
|
||||||
.await
|
.await
|
||||||
.expect("Can't set ascii password");
|
.expect("Can't get ascii password");
|
||||||
let stored_password = entry
|
assert_eq!(
|
||||||
.get_password()
|
stored_password, name,
|
||||||
.await
|
"Retrieved and set ascii passwords don't match"
|
||||||
.expect("Can't get ascii password");
|
);
|
||||||
assert_eq!(
|
entry
|
||||||
stored_password, name,
|
.delete_credential()
|
||||||
"Retrieved and set ascii passwords don't match"
|
.await
|
||||||
);
|
.expect("Can't delete ascii password");
|
||||||
entry
|
assert!(
|
||||||
.delete_credential()
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
.await
|
"Able to read a deleted ascii password"
|
||||||
.expect("Can't delete ascii password");
|
);
|
||||||
assert!(
|
});
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
handles.push(handle);
|
||||||
"Able to read a deleted ascii password"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
handles.push(std::thread::spawn(test))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for handle in handles {
|
for handle in handles {
|
||||||
handle.join().expect("Couldn't execute on thread")
|
handle.await.expect("Task failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,32 +103,27 @@ async fn test_create_set_then_move() {
|
||||||
.set_password(password)
|
.set_password(password)
|
||||||
.await
|
.await
|
||||||
.expect("Can't set ascii password");
|
.expect("Can't set ascii password");
|
||||||
let test = move || {
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
let handle = tokio::spawn(async move {
|
||||||
.enable_all()
|
let stored_password = entry
|
||||||
.build()
|
.get_password()
|
||||||
.unwrap();
|
.await
|
||||||
runtime.block_on(async move {
|
.expect("Can't get ascii password");
|
||||||
let stored_password = entry
|
assert_eq!(
|
||||||
.get_password()
|
stored_password, password,
|
||||||
.await
|
"Retrieved and set ascii passwords don't match"
|
||||||
.expect("Can't get ascii password");
|
);
|
||||||
assert_eq!(
|
entry
|
||||||
stored_password, password,
|
.delete_credential()
|
||||||
"Retrieved and set ascii passwords don't match"
|
.await
|
||||||
);
|
.expect("Can't delete ascii password");
|
||||||
entry
|
assert!(
|
||||||
.delete_credential()
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
.await
|
"Able to read a deleted ascii password"
|
||||||
.expect("Can't delete ascii password");
|
);
|
||||||
assert!(
|
});
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
|
||||||
"Able to read a deleted ascii password"
|
handle.await.expect("Task failed");
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let handle = std::thread::spawn(test);
|
|
||||||
assert!(handle.join().is_ok(), "Couldn't execute on thread")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -151,34 +139,30 @@ async fn test_simultaneous_create_set_then_move() {
|
||||||
.set_password(&name)
|
.set_password(&name)
|
||||||
.await
|
.await
|
||||||
.expect("Can't set ascii password");
|
.expect("Can't set ascii password");
|
||||||
let test = move || {
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
let handle = tokio::spawn(async move {
|
||||||
.enable_all()
|
let stored_password = entry
|
||||||
.build()
|
.get_password()
|
||||||
.unwrap();
|
.await
|
||||||
runtime.block_on(async move {
|
.expect("Can't get ascii password");
|
||||||
let stored_password = entry
|
assert_eq!(
|
||||||
.get_password()
|
stored_password, name,
|
||||||
.await
|
"Retrieved and set ascii passwords don't match"
|
||||||
.expect("Can't get ascii password");
|
);
|
||||||
assert_eq!(
|
entry
|
||||||
stored_password, name,
|
.delete_credential()
|
||||||
"Retrieved and set ascii passwords don't match"
|
.await
|
||||||
);
|
.expect("Can't delete ascii password");
|
||||||
entry
|
assert!(
|
||||||
.delete_credential()
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
.await
|
"Able to read a deleted ascii password"
|
||||||
.expect("Can't delete ascii password");
|
);
|
||||||
assert!(
|
});
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
handles.push(handle);
|
||||||
"Able to read a deleted ascii password"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
handles.push(std::thread::spawn(test))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for handle in handles {
|
for handle in handles {
|
||||||
handle.join().expect("Couldn't execute on thread")
|
handle.await.expect("Task failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,39 +173,34 @@ async fn test_simultaneous_independent_create_set() {
|
||||||
let mut handles = vec![];
|
let mut handles = vec![];
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let name = format!("thread_entry{i}");
|
let name = format!("thread_entry{i}");
|
||||||
let test = move || {
|
let handle = tokio::spawn(async move {
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
let entry = Entry::new(&name, &name).expect("Can't create entry");
|
||||||
.enable_all()
|
entry
|
||||||
.build()
|
.set_password(&name)
|
||||||
.unwrap();
|
.await
|
||||||
runtime.block_on(async move {
|
.expect("Can't set ascii password");
|
||||||
let entry = Entry::new(&name, &name).expect("Can't create entry");
|
let stored_password = entry
|
||||||
entry
|
.get_password()
|
||||||
.set_password(&name)
|
.await
|
||||||
.await
|
.expect("Can't get ascii password");
|
||||||
.expect("Can't set ascii password");
|
assert_eq!(
|
||||||
let stored_password = entry
|
stored_password, name,
|
||||||
.get_password()
|
"Retrieved and set ascii passwords don't match"
|
||||||
.await
|
);
|
||||||
.expect("Can't get ascii password");
|
entry
|
||||||
assert_eq!(
|
.delete_credential()
|
||||||
stored_password, name,
|
.await
|
||||||
"Retrieved and set ascii passwords don't match"
|
.expect("Can't delete ascii password");
|
||||||
);
|
assert!(
|
||||||
entry
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
.delete_credential()
|
"Able to read a deleted ascii password"
|
||||||
.await
|
);
|
||||||
.expect("Can't delete ascii password");
|
});
|
||||||
assert!(
|
handles.push(handle);
|
||||||
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 {
|
for handle in handles {
|
||||||
handle.join().expect("Couldn't execute on thread")
|
handle.await.expect("Task failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,42 +244,37 @@ async fn test_simultaneous_multiple_create_delete_single_thread() {
|
||||||
let mut handles = vec![];
|
let mut handles = vec![];
|
||||||
for t in 0..10 {
|
for t in 0..10 {
|
||||||
let name = generate_random_string();
|
let name = generate_random_string();
|
||||||
let test = move || {
|
let handle = tokio::spawn(async move {
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
let name = format!("{name}-{t}");
|
||||||
.enable_all()
|
let entry = Entry::new(&name, &name).expect("Can't create entry");
|
||||||
.build()
|
let repeats = 10;
|
||||||
.unwrap();
|
for _i in 0..repeats {
|
||||||
runtime.block_on(async move {
|
entry
|
||||||
let name = format!("{name}-{t}");
|
.set_password(&name)
|
||||||
let entry = Entry::new(&name, &name).expect("Can't create entry");
|
.await
|
||||||
let repeats = 10;
|
.expect("Can't set ascii password");
|
||||||
for _i in 0..repeats {
|
let stored_password = entry
|
||||||
entry
|
.get_password()
|
||||||
.set_password(&name)
|
.await
|
||||||
.await
|
.expect("Can't get ascii password");
|
||||||
.expect("Can't set ascii password");
|
assert_eq!(
|
||||||
let stored_password = entry
|
stored_password, name,
|
||||||
.get_password()
|
"Retrieved and set ascii passwords don't match"
|
||||||
.await
|
);
|
||||||
.expect("Can't get ascii password");
|
entry
|
||||||
assert_eq!(
|
.delete_credential()
|
||||||
stored_password, name,
|
.await
|
||||||
"Retrieved and set ascii passwords don't match"
|
.expect("Can't delete ascii password");
|
||||||
);
|
assert!(
|
||||||
entry
|
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
||||||
.delete_credential()
|
"Able to read a deleted ascii password"
|
||||||
.await
|
);
|
||||||
.expect("Can't delete ascii password");
|
}
|
||||||
assert!(
|
});
|
||||||
matches!(entry.get_password().await, Err(Error::NoEntry)),
|
handles.push(handle);
|
||||||
"Able to read a deleted ascii password"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
handles.push(std::thread::spawn(test))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for handle in handles {
|
for handle in handles {
|
||||||
handle.join().expect("Couldn't execute on thread")
|
handle.await.expect("Task failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue