diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 27a853b..0000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: build - -on: [workflow_dispatch, push, pull_request] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - name: Linux build dependencies - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt update -y - sudo apt install -y build-essential gnome-keyring - - - name: Fetch head - uses: actions/checkout@v4 - - - name: Install Rust Toolchain (stable) - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Build (debug) - run: cargo build --verbose --all-targets - - - name: Linux tests - if: matrix.os == 'ubuntu-latest' - run: dbus-run-session -- bash linux-test.sh - - - name: Non-linux tests - if: matrix.os != 'ubuntu-latest' - run: cargo test --verbose - - - name: Format check - run: cargo fmt --all -- --check - - - name: Clippy check - run: cargo clippy -- -D warnings diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..a140e60 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,79 @@ +name: CI + +on: [ workflow_dispatch, push, pull_request ] + +jobs: + ci_native: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + + steps: + - name: Fetch head + uses: actions/checkout@v4 + + - name: Install rust stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt, clippy + + - name: Format check + run: cargo fmt --all -- --check + + - name: Clippy check + run: cargo clippy -- -D warnings + + - name: Build and Test + run: cargo test --features=apple-native,windows-native,linux-native --verbose + + - name: Build the CLI release + run: cargo build --release --features=apple-native,windows-native,linux-native --example cli + + ci_secret_service: + runs-on: ubuntu-latest + strategy: + matrix: + features: + - "linux-keyutils" + - "sync-secret-service" + - "sync-secret-service,crypto-rust" + - "sync-secret-service,crypto-openssl" + - "async-secret-service,tokio,crypto-rust" + - "async-secret-service,async-io,crypto-rust" + - "async-secret-service,tokio,crypto-openssl" + - "async-secret-service,async-io,crypto-openssl" + + steps: + - name: Install CI dependencies + run: | + sudo apt update -y + sudo apt install -y libdbus-1-dev libssl-dev gnome-keyring + + - name: Fetch head + uses: actions/checkout@v4 + + - name: Install rust stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: $test-cache-${{ steps.toolchain.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Start gnome-keyring + # run gnome-keyring with 'foobar' as password for the login keyring + # this will create a new login keyring and unlock it + # the login password doesn't matter, but the keyring must be unlocked for the tests to work + run: gnome-keyring-daemon --components=secrets --daemonize --unlock <<< 'foobar' + + - name: Run tests + # run tests single-threaded to avoid dbus race conditions + run: cargo test --features=${{ matrix.feature }} -- --test-threads=1 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 714bec2..dd41c3d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-latest, ubuntu-latest, windows-latest] + os: [ macos-latest, ubuntu-latest, windows-latest ] include: - os: windows-latest executable_name: examples/cli.exe @@ -27,13 +27,13 @@ jobs: - name: Fetch head uses: actions/checkout@v4 - - name: Install Rust Toolchain (stable) - uses: dtolnay/rust-toolchain@stable + - name: Install rust stable + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - components: rustfmt, clippy + toolchain: stable - - name: Build (release, locked) - run: cargo build --release --example cli + - name: Build + run: cargo build --release --features=apple-native,windows-native,linux-native --example cli - name: Post cli executable uses: svenstaro/upload-release-action@v2 diff --git a/Cargo.toml b/Cargo.toml index 7719864..a6ae275 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,32 +6,27 @@ keywords = ["password", "credential", "keychain", "keyring", "cross-platform"] license = "MIT OR Apache-2.0" name = "keyring" repository = "https://github.com/hwchen/keyring-rs.git" -version = "2.3.3" -rust-version = "1.68" +version = "3.0.0-rc.1" +rust-version = "1.70" edition = "2021" exclude = [".github/"] readme = "README.md" [features] -default = ["platform-all"] -platform-all = ["platform-linux", "platform-freebsd", "platform-openbsd", "platform-macos", "platform-ios", "platform-windows"] -platform-linux = ["linux-secret-service", "linux-keyutils"] -platform-freebsd = ["linux-secret-service"] -platform-openbsd = ["linux-secret-service"] -platform-macos = ["security-framework"] -platform-ios = ["security-framework"] -platform-windows = ["windows-sys", "byteorder"] -linux-secret-service = ["linux-secret-service-rt-async-io-crypto-rust"] -linux-secret-service-rt-async-io-crypto-rust = ["secret-service/rt-async-io-crypto-rust"] -linux-secret-service-rt-tokio-crypto-rust = ["secret-service/rt-tokio-crypto-rust"] -linux-secret-service-rt-async-io-crypto-openssl = ["secret-service/rt-async-io-crypto-openssl"] -linux-secret-service-rt-tokio-crypto-openssl = ["secret-service/rt-tokio-crypto-openssl"] -linux-no-secret-service = ["linux-default-keyutils"] -linux-default-keyutils = ["linux-keyutils"] -windows-test-threading = [] +linux-native = ["dep:linux-keyutils"] +apple-native = ["dep:security-framework"] +windows-native = ["dep:windows-sys", "dep:byteorder"] + +sync-secret-service = ["dep:dbus-secret-service"] +async-secret-service = ["dep:secret-service", "dep:zbus"] +crypto-rust = ["dbus-secret-service?/crypto-rust", "secret-service?/crypto-rust"] +crypto-openssl = ["dbus-secret-service?/crypto-openssl", "secret-service?/crypto-openssl"] +tokio = ["zbus?/tokio"] +async-io = ["zbus?/async-io"] +vendored = ["dbus-secret-service?/vendored", "openssl?/vendored"] [dependencies] -lazy_static = "1" +openssl = { version = "0.10.55", optional = true } [target.'cfg(target_os = "macos")'.dependencies] security-framework = { version = "2.6", optional = true } @@ -40,14 +35,20 @@ security-framework = { version = "2.6", optional = true } security-framework = { version = "2.6", optional = true } [target.'cfg(target_os = "linux")'.dependencies] -secret-service = { version = "3", optional = true } +secret-service = { version = "4", optional = true } +zbus = { version = "4", optional = true } linux-keyutils = { version = "0.2", features = ["std"], optional = true } +dbus-secret-service = { version = "4.0.0-rc.2", optional = true } [target.'cfg(target_os = "freebsd")'.dependencies] -secret-service = { version = "3", optional = true } +secret-service = { version = "4", optional = true } +zbus = { version = "4", optional = true } +dbus-secret-service = { version = "4.0.0-rc.1", optional = true } [target.'cfg(target_os = "openbsd")'.dependencies] -secret-service = { version = "3", optional = true } +secret-service = { version = "4", optional = true } +zbus = { version = "4", optional = true } +dbus-secret-service = { version = "4.0.0-rc.1", optional = true } [target.'cfg(target_os = "windows")'.dependencies] byteorder = { version = "1.2", optional = true } diff --git a/README.md b/README.md index d5a7c49..60a5d29 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ ## Keyring-rs + [![build](https://github.com/hwchen/keyring-rs/actions/workflows/build.yaml/badge.svg)](https://github.com/hwchen/keyring-rs/actions) [![dependencies](https://deps.rs/repo/github/hwchen/keyring-rs/status.svg)](https://github.com/hwchen/keyring-rs) [![crates.io](https://img.shields.io/crates/v/keyring.svg?style=flat-square)](https://crates.io/crates/keyring) [![docs.rs](https://docs.rs/keyring/badge.svg)](https://docs.rs/keyring) A cross-platform library to manage storage and retrieval of passwords -(and other secrets) in the underlying platform secure store, +(and other secrets) in the underlying platform secure store, with a fully-developed example that provides a command-line interface. ## Usage @@ -14,16 +15,16 @@ To use this library in your project add the following to your `Cargo.toml` file: ```toml [dependencies] -keyring = "2" +keyring = "3" ``` This will give you access to the `keyring` crate in your code. -Now you can use the `Entry::new` function to create a new keyring entry. -The `new` function takes a service name -and a user name which together identify the entry. +Now 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 can be added to an entry using its `set_password` method. -They can then be read back using the `get_password` method, +They can then be read back using the `get_password` method, and removed using the `delete_password` method. ```rust @@ -41,24 +42,26 @@ fn main() -> Result<()> { ## 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, +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. ## Examples -The keychain-rs project contains a sample application (`cli`) +The keychain-rs project contains a sample application (`cli`) and a sample library (`ios`). -The `cli` application is a command-line interface to the keyring. +The `cli` application is a command-line interface to the keyring. It can be used to explore how the library is used. It can also be used in debugging keyring-based applications -to probe the contents of the credential store. +to probe the contents of the credential store, but you will +want to rebuild it to use the same credential stores +that are used by your application. -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 +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). While the library can be compiled and linked to on macOS as well, doing so doesn't provide any advantages over the standard macOS tests. @@ -66,88 +69,53 @@ doing so doesn't provide any advantages over the standard macOS tests. ## Client Testing This crate comes with a mock credential store -that can be used by clients who want to test +that can be used by clients who want to test without accessing the native platform store. -The mock store is cross-platform +The mock store is cross-platform and allows mocking errors as well as successes. ## Extensibility -This crate allows clients -to "bring their own credential store" +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/latest/keyring/) +See the [developer docs](https://docs.rs/keyring/) for details. ## Platforms -This crate provides secure storage support for -Linux (secret-service and kernel keyutils), -iOS (keychain), macOS (keychain), and -Windows (credential manager). -It also builds on FreeBSD and OpenBSD (secret-service), -and probably works there, -but since neither the maintainers nor GitHub do +This crate provides built-in implementations of +the following platform-specific credential stores: + +* _Linux_: The DBus-based Secret Service and the kernel keyutils. +* _FreeBSD_, _OpenBSD_: The DBus-based Secret Service. +* _macOS_, _iOS_: The local keychain. +* _Windows_: The Windows Credential Manager. + +To enable the stores you want, you use features. If you +don't enable any stores for a given platform, the _mock_ +keystore will be used. See the [developer docs](https://docs.rs/keyring/) for details. + +Please note: Since neither the maintainers nor GitHub do testing on BSD variants, we rely on contributors -to support these platforms. Thanks for your help! - -The default features of this crate are set up -to build all the available platform support. -So, for example, if you build on macOS, then -keychain support is enabled by loading -other underlying crates that the keychain -credential store requires. - -On Linux, there are two supported platform -credential stores: the secret-service and -the kernel keyutils, and both are built by default. -If you only want to use one or the other, then -you must turn off default features in your -dependency specification and explicitly -specify the feature for the platform support you -want. For example, you might use -```toml -keyring = { version = "2", default_features = false, features = ["linux-secret-service"] } -``` +to support these platforms. Thanks for your help! + +## Upgrading from v2 + +The major functional change between v2 and v3 is the addition of +synchronous support for the Secret Service via the +[dbus-secret-service crate](https://crates.io/crates/dbus-secret-service). This means that +keyring users of the Secret Service no longer +need to link with an async runtime. + +The only API change between v2 and v3 is that the +default feature set has gone away: you must now specify +explicitly which crate-supported keystores you want included. +So keyring clients will need to update their `Cargo.toml` +file, but not their code. -If you don't build any of the platform support features, -then you will get the `mock` keystore as your default. - -PLEASE NOTE: As of version 2.2, turning off the default -feature set will turn off platform support on *all* platforms, -not just on Linux (as was the case before). While this -behavior is a breaking change on Mac, Windows, -FreeBSD and OpenBSD, the behavior on those platforms before was -unintended and undefined (suppressing default features did nothing), -so this is considered a bug fix rather than -a semver-breaking change that requires a major version bump. - -ALSO NOTE: Although the TOML file for this crate specifies a minimum -Rust version of 1.68, that version apples to the library builds _only_. -The TOML has development dependencies that require Rust 1.70. We -keep each major version of the library compiling on Rust versions -that are at least as old as the initial release of that major version. - -## Upgrading from v1 - -The v2 release, -although it adds a lot of functionality relative to v1, -is fully compatible with respect to persisted entry data: -it will both read and set passwords on entries that were -originally written by v1, and entries written -by v2 will be readable and updatable by v1. - -From a client API point of view, the biggest difference -between v2 and v1 is that entry creation using `Entry::new` -and `Entry::new_with_target` can now fail, so v1 client -code will need to add an `unwrap` or other error handling -in order to work with v2. - -There are also new `Error` variants in v2, and the enum -has been declared non-exhaustive (to allow for variants -to be added without breaking client code). -This means that v1 client code that relies on exhaustive -matching will need to be updated. +All v2 data is fully forward-compatible with v3 data; +there have been no changes at all in that respect. ## License @@ -160,7 +128,7 @@ at your option. ## Contributors -Thanks to the following for helping make this library better, +Thanks to the following for helping make this library better, whether through contributing code, discussion, or bug reports! - @Alexei-Barnes @@ -195,14 +163,14 @@ whether through contributing code, discussion, or bug reports! - @VorpalBlade - @thewh1teagle -If you should be on this list, but don't find yourself, +If you should be on this list, but don't find yourself, please contact @brotskydotcom. ### Contribution -Unless you explicitly state otherwise, -any contribution intentionally submitted -for inclusion in the work by you, -as defined in the Apache-2.0 license, -shall be dual licensed as above, +Unless you explicitly state otherwise, +any contribution intentionally submitted +for inclusion in the work by you, +as defined in the Apache-2.0 license, +shall be dual licensed as above, without any additional terms or conditions. diff --git a/linux-test.sh b/linux-test.sh deleted file mode 100644 index 5df5ceb..0000000 --- a/linux-test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -rm -f $HOME/.local/share/keyrings/* -echo -n "test" | gnome-keyring-daemon --unlock -cargo test --verbose diff --git a/src/error.rs b/src/error.rs index 7b20917..b6e3757 100644 --- a/src/error.rs +++ b/src/error.rs @@ -25,13 +25,13 @@ pub enum Error { /// This indicates runtime failure in the underlying /// platform storage system. The details of the failure can /// be retrieved from the attached platform error. - PlatformFailure(Box), + PlatformFailure(Box), /// This indicates that the underlying secure storage /// holding saved items could not be accessed. Typically this /// is because of access rules in the platform; for example, it /// might be that the credential store is locked. The underlying /// platform error will typically give the reason. - NoStorageAccess(Box), + NoStorageAccess(Box), /// This indicates that there is no underlying credential /// entry in the platform for this entry. Either one was /// never set, or it was deleted. diff --git a/src/lib.rs b/src/lib.rs index 292001e..5c74232 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ This crate runs on several different platforms, and it provides one or more implementations of credential stores on each platform. These implementations work by mapping the data used to identify an entry to data used to identify platform-specific storage objects. -For example, on macOS, the service and user names provided for an entry +For example, on macOS, the service and user provided for an entry are mapped to the service and user attributes that identify an element in the macOS keychain. @@ -52,6 +52,49 @@ reference to the underlying concrete object typed as [Any](std::any::Any), so that it can be downgraded to its concrete type. +### Credential store features + +Each of the platform-specific credential stores is associated with one or more features. +These features control whether that store is included when the crate is built. For +example, the macOS Keychain credential store is only included if the `"apple-native"` +feature is specified (and the crate is built with a macOS target). + +If no specified credential store features apply to a given platform, +this crate will use the (platform-independent) _mock_ credential store (see below) +on that platform. Specifying multiple credential store features for a given +platform is not supported, and will cause compile-time errors. There are no +default features in this crate: you must specify explicitly which platform-specific +credential stores you intend to use. + +Here are the available credential store features: + +* `apple-native`: Provides access to the Keychain credential store on macOS and iOS. + +* `windows-native`: Provides access to the Windows Credential Store on Windows. + +* `linux-native`: Provides access to the `keyutils` storage on Linux. + +* `sync-secret-service`: Provides access to the DBus-based +[Secret Service](https://specifications.freedesktop.org/secret-service/latest/) +storage on Linux, FreeBSD, and OpenBSD. This is a _synchronous_ keystore that provides +support for encrypting secrets when they are transferred across the bus. If you wish +to use this encryption support, additionally specify one (and only one) of the +`crypto-rust` or `crypto-openssl` features (to choose the implementation libraries +used for the encryption). By default, this keystore requires that the DBus library be +installed on the user's machine (and the openSSL library if you specify it for +encryption), but you can avoid this requirement by specifying the `vendored` feature +(which will cause the build to include those libraries statically). + +* `async-secret-service`: Provides access to the DBus-based +[Secret Service](https://specifications.freedesktop.org/secret-service/latest/) +storage on Linux, FreeBSD, and OpenBSD. This is an _asynchronous_ keystore that +always encrypts secrets when they are transferred across the bus. You _must_ specify +both an async runtime feature (either `tokio` or `async-io`) and a cryptographic +implementation (either `crypto-rust` or `crypto-openssl`) when using this +keystore. If you want to use openSSL encryption but those libraries are not +installed on the user's machine, specify the `vendored` feature +to statically link them with the built crate. + ## Client-provided Credential Stores In addition to the platform stores implemented by this crate, clients @@ -63,6 +106,7 @@ for use by the [Entry::new] and [Entry::new_with_target] calls. This is done by making a call to [set_default_credential_builder]. The major advantage of this approach is that client code remains independent of the credential builder being used. + - Clients can construct their concrete credentials directly and then turn them into entries by using the [Entry::new_with_credential] call. The major advantage of this approach is that credentials @@ -72,18 +116,18 @@ to the simple model used by this crate. ## Mock Credential Store In addition to the platform-specific credential stores, this crate -also 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 store allows for pre-setting errors as well as password values to be returned from [Entry] method calls. ## Interoperability with Third Parties -Each of the credential stores provided by this crate uses an underlying -platform-specific store that may also be used by modules written +Each of the platform-specific credential stores provided by this crate uses +an underlying store that may also be used by modules written in other languages. If you want to interoperate with these third party credential writers, then you will need to understand the details of how the -target, service name, and user name of this crate's generic model +target, service, and user of this crate's generic model are used to identify credentials in the platform-specific store. These details are in the implementation of this crate's secure-storage modules, and are documented in the headers of those modules. @@ -100,88 +144,93 @@ then retrieving that password will return a [BadEncoding](Error::BadEncoding) er The returned error will have the raw bytes attached, so you can access them. -While this crate's code is thread-safe, -accessing the _same_ entry from multiple threads -in close proximity may be unreliable (especially on Windows), -in that the underlying platform -store may actually execute those calls in a different -order than they are made. As long as you access a single entry from -only one thread at a time, multi-threading should be fine. - -(N.B. Creating an entry is not the same as accessing it, because -entry creation doesn't go through the platform credential manager. -It's fine to create an entry on one thread and then immediately use -it on a different thread. This is thoroughly tested on all platforms.) +While this crate's code is thread-safe, the underlying credential +stores may not handle access from different threads reliably. +In particular, accessing the same credential +from multiple threads at the same time can fail, especially on +Windows and Linux, because the accesses may not be serialized in the same order +they are made. And for RPC-based credential stores such as the dbus-based Secret +Service, accesses from multiple threads (and even the same thread very quickly) +are not recommended, as they may cause the RPC mechanism to fail. */ pub use credential::{Credential, CredentialBuilder}; pub use error::{Error, Result}; -// Included keystore implementations and default choice thereof. - pub mod mock; +// +// no duplicate keystores on any platform +// +#[cfg(any( + all(feature = "linux-keyutils", feature = "sync-secret-service"), + all(feature = "linux-keyutils", feature = "async-secret-service"), + all(feature = "sync-secret-service", feature = "async-secret-service") +))] +compile_error!("You can enable at most one keystore per target architecture"); + +// +// Pick the *nix keystore +// + #[cfg(all(target_os = "linux", feature = "linux-keyutils"))] pub mod keyutils; +#[cfg(all(target_os = "linux", feature = "linux-keyutils"))] +use keyutils as default; + #[cfg(all( - target_os = "linux", - feature = "secret-service", - not(feature = "linux-no-secret-service") + any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"), + any(feature = "sync-secret-service", feature = "async-secret-service") ))] pub mod secret_service; #[cfg(all( - target_os = "linux", - feature = "secret-service", - not(feature = "linux-default-keyutils") + any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"), + any(feature = "sync-secret-service", feature = "async-secret-service") ))] -use crate::secret_service as default; +use secret_service as default; + #[cfg(all( target_os = "linux", - feature = "linux-keyutils", - any(feature = "linux-default-keyutils", not(feature = "secret-service")) + not(any( + feature = "linux-keyutils", + feature = "sync-secret-service", + feature = "async-secret-service" + )) ))] -use keyutils as default; +use mock as default; #[cfg(all( - target_os = "linux", - not(feature = "secret-service"), - not(feature = "linux-keyutils") + any(target_os = "freebsd", target_os = "openbsd"), + not(any(feature = "sync-secret-service", feature = "async-secret-service")) ))] use mock as default; -#[cfg(all(target_os = "freebsd", feature = "secret-service"))] -pub mod secret_service; -#[cfg(all(target_os = "freebsd", feature = "secret-service"))] -use crate::secret_service as default; -#[cfg(all(target_os = "freebsd", not(feature = "secret-service")))] +// +// pick the Apple keystore +// +#[cfg(all(target_os = "macos", feature = "apple-native"))] +pub mod macos; +#[cfg(all(target_os = "macos", feature = "apple-native"))] +use macos as default; +#[cfg(all(target_os = "macos", not(feature = "apple-native")))] use mock as default; -#[cfg(all(target_os = "openbsd", feature = "secret-service"))] -pub mod secret_service; -#[cfg(all(target_os = "openbsd", feature = "secret-service"))] -use crate::secret_service as default; -#[cfg(all(target_os = "openbsd", not(feature = "secret-service")))] +#[cfg(all(target_os = "ios", feature = "apple-native"))] +pub mod ios; +#[cfg(all(target_os = "ios", feature = "apple-native"))] +use ios as default; +#[cfg(all(target_os = "ios", not(feature = "apple-native")))] use mock as default; -#[cfg(all(target_os = "macos", feature = "platform-macos"))] -pub mod macos; -#[cfg(all(target_os = "macos", feature = "platform-macos"))] -use macos as default; -#[cfg(all(target_os = "macos", not(feature = "platform-macos")))] -use mock as default; +// +// pick the Windows keystore +// -#[cfg(all(target_os = "windows", feature = "platform-windows"))] +#[cfg(all(target_os = "windows", feature = "windows-native"))] pub mod windows; -#[cfg(all(target_os = "windows", not(feature = "platform-windows")))] +#[cfg(all(target_os = "windows", not(feature = "windows-native")))] use mock as default; -#[cfg(all(target_os = "windows", feature = "platform-windows"))] +#[cfg(all(target_os = "windows", feature = "windows-native"))] use windows as default; -#[cfg(all(target_os = "ios", feature = "platform-ios"))] -pub mod ios; -#[cfg(all(target_os = "ios", feature = "platform-ios"))] -use ios as default; -#[cfg(all(target_os = "ios", not(feature = "platform-ios")))] -use mock as default; - #[cfg(not(any( target_os = "linux", target_os = "freebsd", @@ -221,16 +270,14 @@ pub fn set_default_credential_builder(new: Box) { } fn build_default_credential(target: Option<&str>, service: &str, user: &str) -> Result { - lazy_static::lazy_static! { - static ref DEFAULT: Box = default::default_credential_builder(); - } + static DEFAULT: std::sync::OnceLock> = std::sync::OnceLock::new(); let guard = DEFAULT_BUILDER .read() .expect("Poisoned RwLock in keyring-rs: please report a bug!"); - let builder = match guard.inner.as_ref() { - Some(builder) => builder, - None => &DEFAULT, - }; + let builder = guard + .inner + .as_ref() + .unwrap_or_else(|| DEFAULT.get_or_init(|| default::default_credential_builder())); let credential = builder.build(target, service, user)?; Ok(Entry { inner: credential }) } @@ -299,7 +346,7 @@ impl Entry { /// Return a reference to this entry's wrapped credential. /// - /// The reference is of the [Any](std::any::Any) type so it can be + /// The reference is of the [Any](std::any::Any) type, so it can be /// downgraded to a concrete credential object. The client must know /// what type of concrete object to cast to. pub fn get_credential(&self) -> &dyn std::any::Any { diff --git a/src/secret_service.rs b/src/secret_service.rs index a9f4484..3367d3c 100644 --- a/src/secret_service.rs +++ b/src/secret_service.rs @@ -49,9 +49,8 @@ be aware that there are known issues with getting dbus and secret-service and the gnome keyring to work properly in headless environments. For a quick workaround, look at how this project's -[CI workflow](https://github.com/hwchen/keyring-rs/blob/master/.github/workflows/build.yaml) -uses the -[linux-test.sh](https://github.com/hwchen/keyring-rs/blob/master/linux-test.sh) script; +[CI workflow](https://github.com/hwchen/keyring-rs/blob/master/.github/workflows/ci.yaml) +starts the Gnome keyring unlocked with a known password; a similar solution is also documented in the [Python Keyring docs](https://pypi.org/project/keyring/) (search for "Using Keyring on headless Linux systems"). @@ -79,8 +78,13 @@ issue for more details and possible workarounds. */ use std::collections::HashMap; -use secret_service::blocking::{Collection, Item, SecretService}; -use secret_service::{EncryptionType, Error}; +#[cfg(not(feature = "async-secret-service"))] +use dbus_secret_service::{Collection, EncryptionType, Error, Item, SecretService}; +#[cfg(feature = "async-secret-service")] +use secret_service::{ + blocking::{Collection, Item, SecretService}, + EncryptionType, Error, +}; use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; use super::error::{decode_password, Error as ErrorCode, Result}; @@ -110,7 +114,11 @@ impl CredentialApi for SsCredential { /// When creating, the item is put into a collection named by the credential's `target` /// attribute. fn set_password(&self, password: &str) -> Result<()> { - let ss = SecretService::connect(EncryptionType::Dh).map_err(platform_failure)?; + #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))] + let session_type = EncryptionType::Dh; + #[cfg(not(any(feature = "crypto-rust", feature = "crypto-openssl")))] + let session_type = EncryptionType::Plain; + let ss = SecretService::connect(session_type).map_err(platform_failure)?; // first try to find a unique, existing, matching item and set its password match self.map_matching_items(|i| set_item_password(i, password), true) { Ok(_) => return Ok(()), @@ -249,7 +257,11 @@ impl SsCredential { F: Fn(&Item) -> Result, T: Sized, { - let ss = SecretService::connect(EncryptionType::Dh).map_err(platform_failure)?; + #[cfg(any(feature = "crypto-rust", feature = "crypto-openssl"))] + let session_type = EncryptionType::Dh; + #[cfg(not(any(feature = "crypto-rust", feature = "crypto-openssl")))] + let session_type = EncryptionType::Plain; + let ss = SecretService::connect(session_type).map_err(platform_failure)?; let attributes: HashMap<&str, &str> = self.search_attributes().into_iter().collect(); let search = ss.search_items(attributes).map_err(decode_error)?; let target = self.target.as_ref().ok_or_else(empty_target)?; @@ -357,7 +369,7 @@ pub fn get_collection<'a>(ss: &'a SecretService, name: &str) -> Result(ss: &'a SecretService<'a>, name: &str) -> Result> { +pub fn create_collection<'a>(ss: &'a SecretService, name: &str) -> Result> { let collection = if name.eq("default") { ss.get_default_collection().map_err(decode_error)? } else { @@ -410,14 +422,9 @@ pub fn matching_target_items<'a>( /// appropriate annotation. pub fn decode_error(err: Error) -> ErrorCode { match err { - Error::Crypto(_) => platform_failure(err), - Error::Zbus(_) => platform_failure(err), - Error::ZbusFdo(_) => platform_failure(err), - Error::Zvariant(_) => platform_failure(err), Error::Locked => no_access(err), Error::NoResult => no_access(err), Error::Prompt => no_access(err), - Error::Unavailable => platform_failure(err), _ => platform_failure(err), } } @@ -434,7 +441,7 @@ fn no_access(err: Error) -> ErrorCode { ErrorCode::NoStorageAccess(wrap(err)) } -fn wrap(err: Error) -> Box { +fn wrap(err: Error) -> Box { Box::new(err) } @@ -525,21 +532,25 @@ mod tests { } fn probe_collection(name: &str) -> bool { - use secret_service::blocking::SecretService; - use secret_service::EncryptionType; + #[cfg(not(feature = "async-secret-service"))] + use dbus_secret_service::{EncryptionType, SecretService}; + #[cfg(feature = "async-secret-service")] + use secret_service::{blocking::SecretService, EncryptionType}; let ss = - SecretService::connect(EncryptionType::Dh).expect("Can't connect to secret service"); + SecretService::connect(EncryptionType::Plain).expect("Can't connect to secret service"); let result = super::get_collection(&ss, name).is_ok(); result } fn delete_collection(name: &str) { - use secret_service::blocking::SecretService; - pub use secret_service::EncryptionType; + #[cfg(not(feature = "async-secret-service"))] + use dbus_secret_service::{EncryptionType, SecretService}; + #[cfg(feature = "async-secret-service")] + use secret_service::{blocking::SecretService, EncryptionType}; let ss = - SecretService::connect(EncryptionType::Dh).expect("Can't connect to secret service"); + SecretService::connect(EncryptionType::Plain).expect("Can't connect to secret service"); let collection = super::get_collection(&ss, name).expect("Can't find collection to delete"); collection.delete().expect("Can't delete collection"); } diff --git a/tests/threading.rs b/tests/threading.rs index 1061c91..4b61626 100644 --- a/tests/threading.rs +++ b/tests/threading.rs @@ -94,7 +94,7 @@ fn test_create_set_then_move() { } #[test] -#[cfg(any(not(target_os = "windows"), feature = "windows-test-threading"))] +#[cfg(not(target_os = "windows"))] fn test_simultaneous_create_set_then_move() { let mut handles = vec![]; for i in 0..10 { @@ -151,14 +151,11 @@ fn test_simultaneous_independent_create_set() { } #[test] -#[cfg(not(all(feature = "linux-keyutils", not(feature = "secret-service"))))] +#[cfg(any(target_os = "macos", target_os = "windows", feature = "linux-native"))] fn test_multiple_create_delete_single_thread() { let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); - #[cfg(not(any(target_os = "macos", target_os = "windows")))] let repeats = 10; - #[cfg(any(target_os = "macos", target_os = "windows"))] - let repeats = 10_000; for _i in 0..repeats { entry.set_password(&name).expect("Can't set ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password"); @@ -177,18 +174,15 @@ fn test_multiple_create_delete_single_thread() { } #[test] -#[cfg(not(all(feature = "linux-keyutils", not(feature = "secret-service"))))] +#[cfg(any(target_os = "macos", target_os = "windows", feature = "linux-native"))] fn test_simultaneous_multiple_create_delete_single_thread() { let mut handles = vec![]; for t in 0..10 { - let root = generate_random_string(); + let name = generate_random_string(); let test = move || { - let name = format!("{root}-{t}"); + let name = format!("{name}-{t}"); let entry = Entry::new(&name, &name).expect("Can't create entry"); - #[cfg(not(any(target_os = "macos", target_os = "windows")))] let repeats = 10; - #[cfg(any(target_os = "macos", target_os = "windows"))] - let repeats = 10_000; for _i in 0..repeats { entry.set_password(&name).expect("Can't set ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password");