keyring-3.6.3/.cargo_vcs_info.json0000644000000001360000000000100125150ustar { "git": { "sha1": "315cbdf6c6a9153d8c9f88b56568f29862d3e39d" }, "path_in_vcs": "" }keyring-3.6.3/.gitignore000064400000000000000000000006011046102023000132720ustar 00000000000000# don't check in Cargo.lock because this is a library project # see https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # don't check in .cargo directory because not every developer # uses it; the typical use is to enable cross-compiling. /.cargo/ # don't check in the binary and testing artifacts target fuzz libtest.rmeta # profiling *.profdata *.profraw keyring-3.6.3/CHANGELOG.md000064400000000000000000000143251046102023000131230ustar 00000000000000## Version 3.6.2 - Have docs.rs build docs for all modules on all platforms (thanks to @unkcpz - see #235). - Switch to `fastrand` for tests (see #237). ## Version 3.6.1 - Updated dependencies; no code changes. ## Version 3.6.0 - Add combination keystore of keyutils and secret service (thanks to @soywod). ## Version 3.5.0 - Add debug logging of internal operations (thanks to @soywod). - Revert iOS security-framework dependency to v2 (see #225). ## Version 3.4.0 - Allow use of both secret-service and keyutils. ## Version 3.3.0 - Add support for credential-store attributes other than those used by this crate. This allows the creation of credentials that are more compatible with 3rd-party clients, such as the OS-provided GUIs over credentials. - Make the textual descriptions of entries consistently follow the form `user@service` (or `user@service:target` if a target was specified). ## Version 3.2.1 - Re-enable access to v1 credentials. The fixes of version 3.2 meant that legacy credentials with no target attribute couldn't be accessed. ## Version 3.2.0 - Improve secret-service handling of targets, so that searches on locked items distinguish items with different targets properly. ## Version 3.1.0 - enhance the CLI to allow empty user names and better info about `Ambiguous` credentials. ## Version 3.0.5 - updated docs and clean up dead code. No code changes. ## Version 3.0.4 - expose a cross-platform module alias via the `default` module. ## Version 3.0.3 - fix feature `linux-native`, which was causing compile errors. ## Version 3.0.2 - add missing implementations for iOS `set_secret` and `get_secret` ## Version 3.0.1 - add back missing `Sync` trait on errors. ## Version 3.0.0 - add `dbus-secret-service` dependency to allow use on \*n\*x without an async runtime - (API change) rework feature controls on included keystores: now there is a feature for each keystore, and that keystore is included in a build if and only if its feature is specified *and* the keystore is supported by the target OS. - (API change) add direct support for setting and reading binary secret data, not just UTF-8 strings. ## Version 2.0.1 - fix the example in the README. ## Version 2.0 - (API change) Allow creation of entries to fail. - (API change) Introduce an ambiguous error on credential lookup. - (API change) Make the `Error` enum non-exhaustive. - (API change) Introduce traits for pluggable credential-store implementations. (This removes the old `platform` module.) - Add a `mock` credential store for easy cross-platform client testing. - Upgrade to secret-service v3. - Always use service-level search in secret-service. - Allow creation of new collections in secret-service. - Add the kernel keyutils as a linux credential store. - Add build support for FreeBSD (thanks @ryanavella). ## Version 1.2.1 - password length was not validated correctly on Windows (#85) ## Version 1.2 - introduce protection against the use of empty arguments ## Version 1.1.2 - replace `structopt` with new, improved `clap` that incorporates all the same functionality. ## Version 1.1.1 - no functional updates, just documentation improvements ## Version 1.1.0 - add iOS support ## Version 1.0.1 - fix #80: missing winapi features (jyuch) ## Version 1.0.0 - Breaking API changes: - `Keyring` struct renamed to `Entry` - `KeyringError` enum renamed to `Error`, and is completely cross-platform. - API enhancements: - Clients can now control how entries map to credentials; see `Entry::new_with_target` and `Entry::new_with_credential` - Clients can now retrieve platform credentials with metadata rather than just passwords; see `Entry::get_password_and_credential`. - Non UTF8 passwords now have their data available. - Non-login keychains are usable on Linux and Mac. - Expanded documentation and `cli` example. ## Version 0.10.4 - CI fix for linux executable ## Version 0.10.3 - Added NoPassword and NoBackend errors to windows code (phillip couto) - Update dependencies: (brotskydotcom) - secret-service from 1.1.1 to 2.0.2 - security-framework from 0.4.2 to 2.4.2 - Update CI/tests, readme (brotskydotcom) ## Version 0.10.2 - yanked, release snafu ## Version 0.10.1 - update to secret-service 1.1.1 ## Version 0.10.0 - ability to access named keychains in macos (nagasunilt) ## Version 0.9.0 - upgrade security-framework 0.3.0 -> 0.4.2 - upgrade secret-service 1.0.0 -> 1.1.0 (updates hkdf dep, fixes error handling related to missing collection) ## Version 0.8.0 - Upgrade to winapi 0.3 and removes advapi32-sys from windows. - Upgrades to edition 2018 - Formats everything to 1.40 - Removes mem::uninitialized from windows. ## Version 0.7.1 - only include application name on create password, not on get password ## Version 0.7.0 - cli binary moved to examples. - osx now uses `security-framework` library instead of cli. - hex dependency removed on osx. - update to `secret-service` for linux, which - removes gmp as a dependency - updates rust-crypto to RustCrypto - correctly encrypts/decrypts blank input - tests moved to `lib.rs` Plan to move to 1.0 if this version is stable. ## Version 0.6.1 - bug fix for special characters on osx. ## Version 0.6.0 - fix behavior in windows where third-party editing of password would result in malformed retrieved password. The solution was to convert all strings to and from Windows utf16, where before I was passing the secret as a blob from utf8. - remove dependency on rustcserialize, use hex. - update rpassword to 2.0, removing dependency on termios - fix some mistakes in syntax for targeting dependencies to an os. ## Version 0.5.1 - remove some unwraps which were causing a problem in linux ## Version 0.5 - bumped secret-service to 0.4.0, which improved error-handling around emptyr passwords a bit more (in 0.3.1), and made gmp dependency optional in 0.4.0 ## Version 0.4 - yanked. But originally was trying to handle secret-service empty password better. But there was an error in secret-service 0.3 ## Version 0.3 - Windows support! ## Version 0.2 - Fix major bug in decoding output on osx. Now handles both regular and "special" (non-ascii) utf8 chars appropriately. - add simple tests for the fix! - add changelog. ## Version 0.1 - linux implementation using secret-service backend. - osx implementation using security cli. keyring-3.6.3/Cargo.lock0000644000001461410000000000100104770ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "pin-project-lite", "slab", ] [[package]] name = "async-fs" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" dependencies = [ "async-lock", "blocking", "futures-lite", ] [[package]] name = "async-io" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "windows-sys 0.60.2", ] [[package]] name = "async-lock" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "async-signal" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", "windows-sys 0.60.2", ] [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ "cipher", ] [[package]] name = "cc" version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", ] [[package]] name = "clap" version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_derive" version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "dbus" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" dependencies = [ "libc", "libdbus-sys", "winapi", ] [[package]] name = "dbus-secret-service" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42a16374481d92aed73ae45b1f120207d8e71d24fb89f357fadbd8f946fd84b" dependencies = [ "aes", "block-padding", "cbc", "dbus", "futures-util", "hkdf", "num", "once_cell", "openssl", "rand", "sha2", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "endi" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "event-listener" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "indexmap" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] [[package]] name = "io-uring" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ "bitflags", "cfg-if", "libc", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "jiff" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde", ] [[package]] name = "jiff-static" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "keyring" version = "3.6.3" dependencies = [ "base64", "byteorder", "clap", "dbus-secret-service", "doc-comment", "env_logger", "fastrand", "linux-keyutils", "log", "openssl", "rpassword", "rprompt", "secret-service", "security-framework 2.11.1", "security-framework 3.2.0", "whoami", "windows-sys 0.60.2", "zbus", "zeroize", ] [[package]] name = "libc" version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libdbus-sys" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" dependencies = [ "cc", "pkg-config", ] [[package]] name = "linux-keyutils" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" dependencies = [ "bitflags", "libc", ] [[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", "memoffset", ] [[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-bigint", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-src" version = "300.5.1+3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a" dependencies = [ "cc", ] [[package]] name = "openssl-sys" version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", "openssl-src", "pkg-config", "vcpkg", ] [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix", "windows-sys 0.60.2", ] [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", ] [[package]] name = "redox_syscall" version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[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]] name = "rustc-demangle" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustix" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.60.2", ] [[package]] name = "secret-service" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" dependencies = [ "aes", "cbc", "futures-util", "generic-array", "hkdf", "num", "once_cell", "openssl", "rand", "serde", "sha2", "zbus", ] [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_repr" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "socket2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "terminal_size" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ "rustix", "windows-sys 0.59.0", ] [[package]] name = "tokio" version = "1.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" dependencies = [ "backtrace", "bytes", "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", "slab", "socket2", "tracing", "windows-sys 0.59.0", ] [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset", "tempfile", "winapi", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "whoami" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ "redox_syscall", "wasite", "web-sys", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.2", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "xdg-home" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "zbus" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" dependencies = [ "async-broadcast", "async-executor", "async-fs", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", "event-listener", "futures-core", "futures-sink", "futures-util", "hex", "nix", "ordered-stream", "rand", "serde", "serde_repr", "sha1", "static_assertions", "tokio", "tracing", "uds_windows", "windows-sys 0.52.0", "xdg-home", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zvariant_utils", ] [[package]] name = "zbus_names" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", "zvariant", ] [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zvariant" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", "syn", ] keyring-3.6.3/Cargo.toml0000644000000106360000000000100105210ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.75" name = "keyring" version = "3.6.3" authors = [ "Walther Chen ", "Daniel Brotsky ", ] build = false exclude = [".github/"] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Cross-platform library for managing passwords/credentials" homepage = "https://github.com/hwchen/keyring-rs" readme = "README.md" keywords = [ "password", "credential", "keychain", "keyring", "cross-platform", ] license = "MIT OR Apache-2.0" repository = "https://github.com/hwchen/keyring-rs.git" [package.metadata.docs.rs] default-target = "x86_64-unknown-linux-gnu" features = [ "apple-native", "windows-native", "linux-native-sync-persistent", "crypto-rust", ] targets = [ "x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "aarch64-apple-ios", "x86_64-pc-windows-msvc", ] [features] apple-native = ["dep:security-framework"] async-io = ["zbus?/async-io"] async-secret-service = [ "dep:secret-service", "dep:zbus", ] crypto-openssl = [ "dbus-secret-service?/crypto-openssl", "secret-service?/crypto-openssl", ] crypto-rust = [ "dbus-secret-service?/crypto-rust", "secret-service?/crypto-rust", ] linux-native = ["dep:linux-keyutils"] linux-native-async-persistent = [ "linux-native", "async-secret-service", ] linux-native-sync-persistent = [ "linux-native", "sync-secret-service", ] sync-secret-service = ["dep:dbus-secret-service"] tokio = ["zbus?/tokio"] vendored = [ "dbus-secret-service?/vendored", "openssl?/vendored", ] windows-native = [ "dep:windows-sys", "dep:byteorder", ] [lib] name = "keyring" path = "src/lib.rs" [[example]] name = "iostest" crate-type = ["staticlib"] path = "examples/ios.rs" [[example]] name = "keyring-cli" path = "examples/cli.rs" [[test]] name = "basic" path = "tests/basic.rs" [[test]] name = "threading" path = "tests/threading.rs" [dependencies.log] version = "0.4.22" [dependencies.openssl] version = "0.10.66" optional = true [dev-dependencies.base64] version = "0.22" [dev-dependencies.clap] version = "4" features = [ "derive", "wrap_help", ] [dev-dependencies.doc-comment] version = "0.3" [dev-dependencies.env_logger] version = "0.11.5" [dev-dependencies.fastrand] version = "2" [dev-dependencies.rpassword] version = "7" [dev-dependencies.rprompt] version = "2" [dev-dependencies.whoami] version = "1.5" [target.'cfg(target_os = "freebsd")'.dependencies.dbus-secret-service] version = "4.0.1" optional = true [target.'cfg(target_os = "freebsd")'.dependencies.secret-service] version = "4" optional = true [target.'cfg(target_os = "freebsd")'.dependencies.zbus] version = "4" optional = true [target.'cfg(target_os = "ios")'.dependencies.security-framework] version = "2" optional = true [target.'cfg(target_os = "linux")'.dependencies.dbus-secret-service] version = "4.0.0-rc.2" optional = true [target.'cfg(target_os = "linux")'.dependencies.linux-keyutils] version = "0.2" features = ["std"] optional = true [target.'cfg(target_os = "linux")'.dependencies.secret-service] version = "4" optional = true [target.'cfg(target_os = "linux")'.dependencies.zbus] version = "4" optional = true [target.'cfg(target_os = "macos")'.dependencies.security-framework] version = "3" optional = true [target.'cfg(target_os = "openbsd")'.dependencies.dbus-secret-service] version = "4.0.0-rc.1" optional = true [target.'cfg(target_os = "openbsd")'.dependencies.secret-service] version = "4" optional = true [target.'cfg(target_os = "openbsd")'.dependencies.zbus] version = "4" optional = true [target.'cfg(target_os = "windows")'.dependencies.byteorder] version = "1.2" optional = true [target.'cfg(target_os = "windows")'.dependencies.windows-sys] version = "0.60" features = [ "Win32_Foundation", "Win32_Security_Credentials", ] optional = true [target.'cfg(target_os = "windows")'.dependencies.zeroize] version = "1.8.1" keyring-3.6.3/Cargo.toml.orig000064400000000000000000000060051046102023000141750ustar 00000000000000[package] authors = ["Walther Chen ", "Daniel Brotsky "] description = "Cross-platform library for managing passwords/credentials" homepage = "https://github.com/hwchen/keyring-rs" keywords = ["password", "credential", "keychain", "keyring", "cross-platform"] license = "MIT OR Apache-2.0" name = "keyring" repository = "https://github.com/hwchen/keyring-rs.git" version = "3.6.3" rust-version = "1.75" edition = "2021" exclude = [".github/"] readme = "README.md" [features] linux-native = ["dep:linux-keyutils"] apple-native = ["dep:security-framework"] windows-native = ["dep:windows-sys", "dep:byteorder"] linux-native-sync-persistent = ["linux-native", "sync-secret-service"] linux-native-async-persistent = ["linux-native", "async-secret-service"] 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] log = "0.4.22" openssl = { version = "0.10.66", optional = true } [target.'cfg(target_os = "macos")'.dependencies] # see issue #190 security-framework = { version = "3", optional = true } [target.'cfg(target_os = "ios")'.dependencies] # see issue #190 security-framework = { version = "2", optional = true } [target.'cfg(target_os = "linux")'.dependencies] 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 = "4", optional = true } zbus = { version = "4", optional = true } dbus-secret-service = { version = "4.0.1", optional = true } [target.'cfg(target_os = "openbsd")'.dependencies] 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 } windows-sys = { version = "0.60", features = ["Win32_Foundation", "Win32_Security_Credentials"], optional = true } zeroize = "1.8.1" [[example]] name = "iostest" path = "examples/ios.rs" crate-type = ["staticlib"] [[example]] name = "keyring-cli" path = "examples/cli.rs" [dev-dependencies] base64 = "0.22" clap = { version = "4", features = ["derive", "wrap_help"] } doc-comment = "0.3" env_logger = "0.11.5" fastrand = "2" rpassword = "7" rprompt = "2" whoami = "1.5" [package.metadata.docs.rs] default-target = "x86_64-unknown-linux-gnu" targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "aarch64-apple-ios", "x86_64-pc-windows-msvc"] features = ["apple-native", "windows-native", "linux-native-sync-persistent", "crypto-rust"] keyring-3.6.3/LICENSE-APACHE000064400000000000000000000251321046102023000132340ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [2017] [keyring developers] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. keyring-3.6.3/LICENSE-MIT000064400000000000000000000020461046102023000127430ustar 00000000000000Copyright (c) 2016 keyring Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. keyring-3.6.3/README.md000064400000000000000000000242751046102023000125760ustar 00000000000000## Keyring-rs [![build](https://github.com/hwchen/keyring-rs/actions/workflows/ci.yaml/badge.svg)](https://github.com/hwchen/keyring-rs/actions) [![dependencies](https://deps.rs/repo/github/hwchen/keyring-rs/status.svg)](https://deps.rs/repo/github/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, with a fully-developed example that provides a command-line interface. ## Usage To use this crate in your project, you must include it in your `Cargo.toml` and specify a feature for each supported credential store you want to use. For example, if you want to use the platform credential stores on Mac and Win, and use the Secret Service (synchronously) on Linux and \*nix platforms, you would add a snippet such as this to your `[dependencies]` section: ```toml keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } ``` 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'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 an entry in the underlying 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 use keyring::{Entry, Result}; fn main() -> Result<()> { let entry = Entry::new("my-service", "my-name")?; entry.set_password("topS3cr3tP4$$w0rd")?; let password = entry.get_password()?; println!("My password is '{}'", password); entry.delete_credential()?; Ok(()) } ``` ## 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. ## Examples 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; just be sure to build it using the same features/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 [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 using the crate directly. ## 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. ## 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 This crate provides built-in implementations of the following platform-specific credential stores: * _Linux_: The DBus-based Secret Service, the kernel keyutils, and a combo of the two. * _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: there is one feature for each possibly-included credential store. If you specify a feature (e.g., `dbus-secret-service`) _and_ your target platform (e.g., `freebsd`) supports that credential store, it will be included as the default credential store in that build. That way you can have a build command that specifies a single credential store for each of your target platforms, and use that same build command for all targets. If you don't enable any credential stores that are supported on a given platform, the _mock_ keystore will be the default on that platform. See the [developer docs](https://docs.rs/keyring/) for details of which features control the inclusion of which credential stores. ### 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 of the following: * Access to credential stores via this crate is always *synchronous*; that is, it blocks the thread on which it was invoked. This is true *even if* your feature set specifies using the Zbus-based `secret-service` crate, which offers an async interface, because this crate always uses the blocking interface. If your application is using an async runtime already, and you build with the `async-secret-service` feature in this crate, you should (1) specify the same async runtime you are using as a feature for this crate, and (2) be sure to have a separate thread that is used for all `keyring` calls that access the credential store. Failure to use a separate thread is known to cause deadlocks. * Because credential store access from this crate is always synchronous, there is really no reason not to use the `sync-secret-service` feature (rather than `async-secret-service`) with this crate, *even if* your code is already using one of the async runtimes. Yes, this feature requires that `libdbus` be installed on your user’s machines, but it is by default in all desktop OS installs that include a Secret Service implementation (such as the Gnome Keyring or the KWallet). If you want to be extra careful, you can use the additional `vendored` feature to this crate to statically link the dbus library with your app so it’s not required on user machines. Just keep in mind that, in the event of an update to `libdbus`, using the `vendored` feature will require a rebuild of your app to get the `libdbus` update to your users. * Every call to the Secret Service is done via an inter-process call, which takes time (typically tens if not hundreds of milliseconds). If, for some reason, your code is pounding on the Secret Service like a database, you will want to implement a write-through transactional backing cache to protect your users from slowdowns. The `mock` credential store can be adapted for this purpose. 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 or user names to be empty, because empty fields are treated as wildcards on lookup. Use some default, non-empty value instead. ## 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. (There are other advantages as well; see [above](#platform-specific-issues) for details.) The main API change between v2 and v3 is the addition of support for non-string (i.e., binary) "password" data. To accommodate this, two changes have been made: 1. There are two new methods on `Entry` objects: `set_secret` and `get_secret`. These are the analogs of `set_password` and `get_password`, but instead of taking or returning strings they take or return binary data (byte arrays/vectors). 2. The v2 method `delete_password` has been renamed `delete_credential`, both to clarify what's actually being deleted and to emphasize that it doesn't matter whether it's holding a "password" or a "secret". Another API change between v2 and v3 is that the notion of a default feature set has gone away: you must now specify explicitly which crate-supported keystores you want included (other than the `mock` keystore, which is always present). So all keyring client developers will need to update their `Cargo.toml` file to use the new features correctly. All v2 data is fully forward-compatible with v3 data; there have been no changes at all in that respect. The MSRV has been moved to 1.75, and all direct dependencies are at their latest stable versions. ## License Licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ## Contributors Thanks to the following for helping make this library better, whether through contributing code, discussion, or bug reports! - @Alexei-Barnes - @benwr - @bhkaminski - @Brooooooklyn - @brotskydotcom - @complexspaces - @connor4312 - @dario23 - @dten - @gondolyr - @hwchen - @jankatins - @jasikpark - @jkhsjdhjs - @jonathanmorley - @jyuch - @klemensn - @landhb - @lexxvir - @MaikKlein - @Phrohdoh - @phlip9 - @ReactorScram - @Rukenshia - @russellbanks - @ryanavella - @samuela - @ShaunSHamilton - @soywod - @stankec - @steveatinfincia - @Sytten - @thewh1teagle - @tmpfs - @unkcpz - @vermiculus - @VorpalBlade - @zschreur 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, without any additional terms or conditions. keyring-3.6.3/examples/cli.rs000064400000000000000000000264141046102023000142470ustar 00000000000000extern crate keyring; use clap::{Args, Parser}; use std::collections::HashMap; use keyring::{Entry, Error, Result}; fn main() { let mut args: Cli = Cli::parse(); if args.user.eq_ignore_ascii_case("") { 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) { Ok(()) => args.success_message_for(&value), Err(err) => args.error_message_for(err), }, Value::Password(password) => match entry.set_password(password) { 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) { 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() { Ok(password) => { println!("{password}"); args.success_message_for(&Value::Password(password)); } Err(err) => args.error_message_for(err), }, Command::Secret => match entry.get_secret() { Ok(secret) => { println!("{}", secret_string(&secret)); args.success_message_for(&Value::Secret(secret)); } Err(err) => args.error_message_for(err), }, Command::Attributes => match entry.get_attributes() { 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() { 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, #[clap(short, long, value_parser, default_value = "keyring-cli")] /// The service for the entry. pub service: String, #[clap(short, long, value_parser, default_value = "")] /// 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, }, /// 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), Password(String), Attributes(HashMap), 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 { 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) { for (key, value) in attributes { println!(" {key}: {value}"); } } fn decode_secret(input: &Option) -> Vec { 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 { if let Some(input) = input { input.clone() } else { rpassword::prompt_password("Password: ").unwrap_or_else(|_| String::new()) } } fn attributes_string(attributes: &HashMap) -> String { let strings = attributes .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>(); strings.join(",") } fn parse_attributes(input: &Option) -> HashMap { 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 } keyring-3.6.3/examples/ios.rs000064400000000000000000000100501046102023000142570ustar 00000000000000use keyring::{Entry, Error}; #[no_mangle] extern "C" fn test() { test_invalid_parameter(); test_empty_keyring(); test_empty_password_input(); test_round_trip_ascii_password(); test_round_trip_non_ascii_password(); test_update_password(); #[cfg(target_os = "ios")] test_get_credential(); } 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" ); } fn test_empty_keyring() { let name = "test_empty_keyring".to_string(); let entry = Entry::new(&name, &name).expect("Failed to create entry"); assert!(matches!(entry.get_password(), Err(Error::NoEntry))) } 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) .expect("Couldn't set empty password"); let out_pass = entry.get_password().expect("Couldn't get empty password"); assert_eq!(in_pass, out_pass); entry .delete_credential() .expect("Couldn't delete credential with empty password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted password" ) } fn test_round_trip_ascii_password() { let name = "test_round_trip_ascii_password".to_string(); let entry = Entry::new(&name, &name).expect("Failed to create entry"); let password = "test ascii password"; entry.set_password(password).unwrap(); let stored_password = entry.get_password().unwrap(); assert_eq!(stored_password, password); entry.delete_credential().unwrap(); assert!(matches!(entry.get_password(), Err(Error::NoEntry))) } fn test_round_trip_non_ascii_password() { let name = "test_round_trip_non_ascii_password".to_string(); let entry = Entry::new(&name, &name).expect("Failed to create entry"); let password = "このきれいな花は桜です"; entry.set_password(password).unwrap(); let stored_password = entry.get_password().unwrap(); assert_eq!(stored_password, password); entry.delete_credential().unwrap(); assert!(matches!(entry.get_password(), Err(Error::NoEntry))) } fn test_update_password() { let name = "test_update_password".to_string(); let entry = Entry::new(&name, &name).expect("Failed to create entry"); let password = "test ascii password"; entry.set_password(password).unwrap(); let stored_password = entry.get_password().unwrap(); assert_eq!(stored_password, password); let password = "このきれいな花は桜です"; entry.set_password(password).unwrap(); let stored_password = entry.get_password().unwrap(); assert_eq!(stored_password, password); entry.delete_credential().unwrap(); assert!(matches!(entry.get_password(), Err(Error::NoEntry))) } #[cfg(target_os = "ios")] fn test_get_credential() { use keyring::ios::IosCredential; let name = "test_get_credential".to_string(); let entry = Entry::new(&name, &name).expect("Can't create entry for get_credential"); let credential: &IosCredential = entry .get_credential() .downcast_ref() .expect("Not an iOS credential"); assert!( credential.get_credential().is_err(), "Platform credential shouldn't exist yet!" ); entry .set_password("test get password for get_credential") .expect("Can't get password for get_credential"); assert!(credential.get_credential().is_ok()); entry.delete_credential().unwrap(); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Platform credential exists after delete password" ) } keyring-3.6.3/release.toml000064400000000000000000000000321046102023000136150ustar 00000000000000tag-name = 'v{{version}}' keyring-3.6.3/src/credential.rs000064400000000000000000000171261046102023000145630ustar 00000000000000/*! # Platform-independent secure storage model This module defines a plug and play model for platform-specific credential stores. The model comprises two traits: [CredentialBuilderApi] for the underlying store 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 [Credential] types that wrap them. Note that you must have an instance of a credential builder in your hands in order to call the [CredentialBuilder] API. Because each credential builder implementation lives in a platform-specific module, the cross-platform way to get your hands on the one currently being used to create entries is to ask for the builder from the `default` module alias. For example, to determine whether the credential builder currently being used persists its credentials across machine reboots, you might use a snippet like this: ```rust use keyring::{default, credential}; let persistence = default::default_credential_builder().persistence(); if matches!(persistence, credential::CredentialPersistence::UntilDelete) { println!("The default credential builder persists credentials on disk!") } else { println!("The default credential builder doesn't persist credentials on disk!") } ``` */ use std::any::Any; use std::collections::HashMap; use super::Result; /// The API that [credentials](Credential) implement. pub trait CredentialApi { /// Set the credential's password (a string). /// /// This will persist the password in the underlying store. fn set_password(&self, password: &str) -> Result<()> { self.set_secret(password.as_bytes()) } /// Set the credential's secret (a byte array). /// /// This will persist the secret in the underlying store. fn set_secret(&self, password: &[u8]) -> Result<()>; /// Retrieve the password (a string) from the underlying credential. /// /// This has no effect on the underlying store. If there is no credential /// for this entry, a [NoEntry](crate::Error::NoEntry) error is returned. fn get_password(&self) -> Result { let secret = self.get_secret()?; super::error::decode_password(secret) } /// Retrieve a secret (a byte array) from the credential. /// /// This has no effect on the underlying store. If there is no credential /// for this entry, a [NoEntry](crate::Error::NoEntry) error is returned. fn get_secret(&self) -> Result>; /// Get the secure store attributes on this entry's credential. /// /// Each credential store may support reading and updating different /// named attributes; see the documentation on each of the stores /// for details. Note that the keyring itself uses some of these /// attributes to map entries to their underlying credential; these /// _controlled_ attributes are not available for reading or updating. /// /// We provide a default (no-op) implementation of this method /// for backward compatibility with stores that don't implement it. fn get_attributes(&self) -> Result> { // this should err in the same cases as get_secret, so first call that for effect self.get_secret()?; // if we got this far, return success with no attributes Ok(HashMap::new()) } /// Update the secure store attributes on this entry's credential. /// /// Each credential store may support reading and updating different /// named attributes; see the documentation on each of the stores /// for details. The implementation will ignore any attribute names /// that you supply that are not available for update. Because the /// names used by the different stores tend to be distinct, you can /// write cross-platform code that will work correctly on each platform. /// /// We provide a default no-op implementation of this method /// for backward compatibility with stores that don't implement it. fn update_attributes(&self, _: &HashMap<&str, &str>) -> Result<()> { // this should err in the same cases as get_secret, so first call that for effect self.get_secret()?; // if we got this far, return success after setting no attributes Ok(()) } /// Delete the underlying credential, if there is one. /// /// This is not idempotent if the credential existed! /// A second call to delete_credential will return /// a [NoEntry](crate::Error::NoEntry) error. fn delete_credential(&self) -> Result<()>; /// Return the underlying concrete object cast to [Any]. /// /// This allows clients /// to downcast the credential to its concrete type so they /// can do platform-specific things with it (e.g., /// query its attributes in the underlying store). fn as_any(&self) -> &dyn Any; /// The Debug trait call for the object. /// /// This is used to implement the Debug trait on this type; it /// allows generic code to provide debug printing as provided by /// the underlying concrete object. /// /// We provide a (useless) default implementation for backward /// compatibility with existing implementors who may have not /// implemented the Debug trait for their credential objects fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self.as_any(), f) } } /// A thread-safe implementation of the [Credential API](CredentialApi). pub type Credential = dyn CredentialApi + Send + Sync; impl std::fmt::Debug for Credential { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.debug_fmt(f) } } /// A descriptor for the lifetime of stored credentials, returned from /// a credential store's [persistence](CredentialBuilderApi::persistence) call. #[non_exhaustive] pub enum CredentialPersistence { /// Credentials vanish when the entry vanishes (stored in the entry) EntryOnly, /// Credentials vanish when the process terminates (stored in process memory) ProcessOnly, /// Credentials persist until the machine reboots (stored in kernel memory) UntilReboot, /// Credentials persist until they are explicitly deleted (stored on disk) UntilDelete, } /// The API that [credential builders](CredentialBuilder) implement. pub trait CredentialBuilderApi { /// Create a credential identified by the given target, service, and user. /// /// This typically has no effect on the content of the underlying store. /// A credential need not be persisted until its password is set. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result>; /// Return the underlying concrete object cast to [Any]. /// /// Because credential builders need not have any internal structure, /// this call is not so much for clients /// as it is to allow automatic derivation of a Debug trait for builders. fn as_any(&self) -> &dyn Any; /// The lifetime of credentials produced by this builder. /// /// A default implementation is provided for backward compatibility, /// since this API was added in a minor release. The default assumes /// that keystores use disk-based credential storage. fn persistence(&self) -> CredentialPersistence { CredentialPersistence::UntilDelete } } impl std::fmt::Debug for CredentialBuilder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.as_any().fmt(f) } } /// A thread-safe implementation of the [CredentialBuilder API](CredentialBuilderApi). pub type CredentialBuilder = dyn CredentialBuilderApi + Send + Sync; keyring-3.6.3/src/error.rs000064400000000000000000000112571046102023000136010ustar 00000000000000/*! Platform-independent error model. There is an escape hatch here for surfacing platform-specific error information returned by the platform-specific storage provider, but the concrete objects returned must be `Send` so they can be moved from one thread to another. (Since most platform errors are integer error codes, this requirement is not much of a burden on the platform-specific store providers.) */ use crate::Credential; #[derive(Debug)] /// Each variant of the `Error` enum provides a summary of the error. /// More details, if relevant, are contained in the associated value, /// which may be platform-specific. /// /// This enum is non-exhaustive so that more values can be added to it /// without a SemVer break. Clients should always have default handling /// for variants they don't understand. #[non_exhaustive] 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), /// 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), /// This indicates that there is no underlying credential /// entry in the platform for this entry. Either one was /// never set, or it was deleted. NoEntry, /// This indicates that the retrieved password blob was not /// a UTF-8 string. The underlying bytes are available /// for examination in the attached value. BadEncoding(Vec), /// This indicates that one of the entry's credential /// attributes exceeded a /// length limit in the underlying platform. The /// attached values give the name of the attribute and /// the platform length limit that was exceeded. TooLong(String, u32), /// This indicates that one of the entry's required credential /// attributes was invalid. The /// attached value gives the name of the attribute /// and the reason it's invalid. Invalid(String, String), /// This indicates that there is more than one credential found in the store /// that matches the entry. Its value is a vector of the matching credentials. Ambiguous(Vec>), } pub type Result = std::result::Result; impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Error::PlatformFailure(err) => write!(f, "Platform secure storage failure: {err}"), Error::NoStorageAccess(err) => { write!(f, "Couldn't access platform secure storage: {err}") } Error::NoEntry => write!(f, "No matching entry found in secure storage"), Error::BadEncoding(_) => write!(f, "Data is not UTF-8 encoded"), Error::TooLong(name, len) => write!( f, "Attribute '{name}' is longer than platform limit of {len} chars" ), Error::Invalid(attr, reason) => { write!(f, "Attribute {attr} is invalid: {reason}") } Error::Ambiguous(items) => { write!( f, "Entry is matched by {} credentials: {items:?}", items.len(), ) } } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::PlatformFailure(err) => Some(err.as_ref()), Error::NoStorageAccess(err) => Some(err.as_ref()), _ => None, } } } /// Try to interpret a byte vector as a password string pub fn decode_password(bytes: Vec) -> Result { String::from_utf8(bytes).map_err(|err| Error::BadEncoding(err.into_bytes())) } #[cfg(test)] mod tests { use super::*; #[test] fn test_bad_password() { // malformed sequences here taken from: // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt for bytes in [b"\x80".to_vec(), b"\xbf".to_vec(), b"\xed\xa0\xa0".to_vec()] { match decode_password(bytes.clone()) { Err(Error::BadEncoding(str)) => assert_eq!(str, bytes), Err(other) => panic!("Bad password ({bytes:?}) decode gave wrong error: {other}"), Ok(s) => panic!("Bad password ({bytes:?}) decode gave results: {s:?}"), } } } } keyring-3.6.3/src/ios.rs000064400000000000000000000162051046102023000132400ustar 00000000000000/*! # iOS Keychain credential store All credentials on iOS are stored in secure stores called _keychains_. On iOS there is only one of these, and it has no name. The target attribute of an [Entry](crate::Entry), for consistency with macOS, determines which keychain an entry's credential is created in searched for. On iOS, then, entries must have no target or use the specially named target `default`. For a given service/user pair, this module creates/searches for a credential in the target keychain whose _account_ attribute holds the user and whose _name_ attribute holds the service. Because of a quirk in the iOS keychain services API, neither the _account_ nor the _name_ may be the empty string. (Empty strings are treated as wildcards when looking up credentials by attribute value.) Credentials on iOS can have a large number of _key/value_ attributes, but this module controls the _account_ and _name_ attributes and ignores all the others. so clients can't use it to access or update any attributes. */ use security_framework::base::Error; use security_framework::passwords::{ delete_generic_password, get_generic_password, set_generic_password, }; use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; use super::error::{decode_password, Error as ErrorCode, Result}; /// The representation of a generic Keychain credential. /// /// The actual credentials can have lots of attributes /// not represented here. There's no way to use this /// module to get at those attributes. #[derive(Debug, Clone, PartialEq, Eq)] pub struct IosCredential { pub service: String, pub account: String, } impl CredentialApi for IosCredential { /// Create and write a credential with password for this entry. /// /// The new credential replaces any existing one in the store. /// Since there is only one credential with a given _account_ and _user_ /// in any given keychain, there is no chance of ambiguity. fn set_password(&self, password: &str) -> Result<()> { self.set_secret(password.as_bytes())?; Ok(()) } /// Create and write a credential with secret for this entry. /// /// The new credential replaces any existing one in the store. /// Since there is only one credential with a given _account_ and _user_ /// in any given keychain, there is no chance of ambiguity. fn set_secret(&self, secret: &[u8]) -> Result<()> { set_generic_password(&self.service, &self.account, secret).map_err(decode_error)?; Ok(()) } /// Look up the password for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn get_password(&self) -> Result { let password_bytes = self.get_secret()?; decode_password(password_bytes) } /// Look up the secret for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn get_secret(&self) -> Result> { get_generic_password(&self.service, &self.account).map_err(decode_error) } /// Delete the underlying generic credential for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn delete_credential(&self) -> Result<()> { delete_generic_password(&self.service, &self.account).map_err(decode_error)?; Ok(()) } /// Return the underlying concrete object with an `Any` type so that it can /// be downgraded to an [IosCredential] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } /// Expose the concrete debug formatter for use via the [Credential] trait fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } } impl IosCredential { /// Construct a credential from the underlying generic credential. /// /// On iOS, this is basically a no-op, because we represent any attributes /// other than the ones we use to find the generic credential. /// But at least this checks whether the underlying credential exists. pub fn get_credential(&self) -> Result { get_generic_password(&self.service, &self.account).map_err(decode_error)?; Ok(self.clone()) } /// Create a credential representing a Mac keychain entry. /// /// The target string is ignored, because there's only one keychain. /// /// Creating a credential does not put anything into the keychain. /// The keychain entry will be created /// when [set_password](IosCredential::set_password) is /// called. /// /// This will fail if the service or user strings are empty, /// because empty attribute values act as wildcards in the /// Keychain Services API. pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result { if service.is_empty() { return Err(ErrorCode::Invalid( "service".to_string(), "cannot be empty".to_string(), )); } if user.is_empty() { return Err(ErrorCode::Invalid( "user".to_string(), "cannot be empty".to_string(), )); } if let Some(target) = target { if target.to_ascii_lowercase() != "default" { return Err(ErrorCode::Invalid( "target".to_string(), "only 'default' is allowed".to_string(), )); } } Ok(Self { service: service.to_string(), account: user.to_string(), }) } } /// The builder for iOS keychain credentials pub struct IosCredentialBuilder {} /// Returns an instance of the iOS credential builder. /// /// On iOS, /// this is called once when an entry is first created. pub fn default_credential_builder() -> Box { Box::new(IosCredentialBuilder {}) } impl CredentialBuilderApi for IosCredentialBuilder { /// Build an [IosCredential] for the given target, service, and user. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { Ok(Box::new(IosCredential::new_with_target( target, service, user, )?)) } /// Return the underlying builder object with an `Any` type so that it can /// be downgraded to an [IosCredentialBuilder] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } } /// Map an iOS API error to a crate error with appropriate annotation /// /// The iOS error code values used here are from /// [this reference](https://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-78/lib/SecBase.h.auto.html) fn decode_error(err: Error) -> ErrorCode { match err.code() { -25291 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecNotAvailable -25292 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecReadOnly -25300 => ErrorCode::NoEntry, // errSecItemNotFound _ => ErrorCode::PlatformFailure(Box::new(err)), } } keyring-3.6.3/src/keyutils.rs000064400000000000000000000365371046102023000143310ustar 00000000000000/*! # Linux kernel (keyutils) credential store Modern linux kernels have a built-in secure store, [keyutils](https://www.man7.org/linux/man-pages/man7/keyutils.7.html). This module (written primarily by [@landhb](https://github.com/landhb)) uses that secure store as the persistent back end for entries. Entries in keyutils are identified by a string `description`. If an entry is created with an explicit `target`, that value is used as the keyutils description. Otherwise, the string `keyring-rs:user@service` is used (where user and service come from the entry creation call). There is no notion of attribute other than the description supported by keyutils, so the [get_attributes](crate::Entry::get_attributes) and [update_attributes](crate::Entry::update_attributes) calls are both no-ops for this credential store. # Persistence The key management facility provided by the kernel is completely in-memory and will not persist across reboots. Consider the keyring a secure cache and plan for your application to handle cases where the entry is no-longer available in-memory. In general you should prepare for `Entry::get_password` to fail and have a fallback to re-load the credential into memory. Potential options to re-load the credential into memory are: - Re-prompt the user (most common/effective for CLI applications) - Create a PAM module or use `pam_exec` to load a credential securely when the user logs in. - If you're running as a systemd service you can use `systemd-ask-password` to prompt the user when your service starts. ``` use std::error::Error; use keyring::Entry; /// Simple user code that handles retrieving a credential regardless /// of the credential state. struct CredentialManager { entry: Entry, } impl CredentialManager { /// Init the service as normal pub fn new(service: &str, user: &str) -> Result> { Ok(Self { entry: Entry::new(service, user)? }) } /// Method that first attempts to retreive the credential from memory /// and falls back to prompting the user. pub fn get(&self) -> Result> { self.entry.get_password().or_else(|_| self.prompt()) } /// Internal method to prompt the user and cache the credential /// in memory for subsequent lookups. fn prompt(&self) -> Result> { let password = rpassword::read_password()?; self.entry.set_password(&password)?; Ok(password) } } ``` A single entry in keyutils can be on multiple "keyrings", each of which has a subtly different lifetime. The core storage for keyring keys is provided by the user-specific [persistent keyring](https://www.man7.org/linux/man-pages/man7/persistent-keyring.7.html), whose lifetime defaults to a few days (and is controllable by administrators). But whenever an entry's credential is used, it is also added to the user's [session keyring](https://www.man7.org/linux/man-pages/man7/session-keyring.7.html): this ensures that the credential will persist as long as the user session exists, and when the user logs out the credential will persist as long as the persistent keyring doesn't expire while the user is logged out. Each time the `Entry::new()` operation is performed, the persistent keyring's expiration timer is reset to the value configured in: ```no_run,no_test,ignore proc/sys/kernel/keys/persistent_keyring_expiry ``` | Persistent Keyring State | Session Keyring State | User Key State | | ------------- | ------------- | ------------- | | Active | Active | Active | | Expired | Active | Active | | Active | Logged Out | Active (Accessible on next login) | | Expired | Logged Out | Expired | **Note**: As mentioned above, a reboot clears all keyrings. ## Headless usage If you are trying to use keyring on a headless linux box, it's strongly recommended that you use this credential store, because (as part of the kernel) it's designed to be used headlessly. To set this module as your default store, build with `--features linux-default-keyutils`. Alternatively, you can drop the secret-service credential store altogether (which will slim your build significantly) by building keyring with `--no-default-features` and `--features linux-no-secret-service`. */ use super::credential::{ Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, }; use super::error::{decode_password, Error as ErrorCode, Result}; use linux_keyutils::{KeyError, KeyRing, KeyRingIdentifier}; /// Representation of a keyutils credential. /// /// Since the CredentialBuilderApi::build method does not provide /// an initial secret, and it is impossible to have 0-length keys, /// this representation holds a linux_keyutils::KeyRing instead /// of a linux_keyutils::Key. /// /// The added benefit of this approach /// is that any call to get_password before set_password is done /// will result in a proper error as the key does not exist until /// set_password is called. #[derive(Debug, Clone)] pub struct KeyutilsCredential { /// Host session keyring pub session: KeyRing, /// Host persistent keyring pub persistent: Option, /// Description of the key entry pub description: String, } impl CredentialApi for KeyutilsCredential { /// Set a password in the underlying store /// /// This will overwrite the entry if it already exists since /// it's using `add_key` under the hood. /// /// Returns an [Invalid](ErrorCode::Invalid) error if the password /// is empty, because keyutils keys cannot have empty values. fn set_password(&self, password: &str) -> Result<()> { self.set_secret(password.as_bytes()) } fn set_secret(&self, secret: &[u8]) -> Result<()> { if secret.is_empty() { return Err(ErrorCode::Invalid( "secret".to_string(), "cannot be empty".to_string(), )); } // Add to the session keyring let key = self .session .add_key(&self.description, secret) .map_err(decode_error)?; // Directly link to the persistent keyring as well if let Some(keyring) = self.persistent { keyring.link_key(key).map_err(decode_error)?; } Ok(()) } /// Retrieve a password from the underlying store /// /// This requires a call to `Key::read` with checked conversions /// to a utf8 Rust string. fn get_password(&self) -> Result { let secret = self.get_secret()?; // Attempt utf-8 conversion decode_password(secret) } /// Retrieve a secret from the underlying store /// /// This requires a call to `Key::read`. fn get_secret(&self) -> Result> { // Verify that the key exists and is valid let key = self .session .search(&self.description) .map_err(decode_error)?; // Directly re-link to the session keyring // If a logout occurred, it will only be linked to the // persistent keyring, and needs to be added again. self.session.link_key(key).map_err(decode_error)?; // Directly re-link to the persistent keyring // If it expired, it will only be linked to the // session keyring, and needs to be added again. if let Some(keyring) = self.persistent { keyring.link_key(key).map_err(decode_error)?; } // Read in the key (making sure we have enough room) let buffer = key.read_to_vec().map_err(decode_error)?; Ok(buffer) } /// Delete a password from the underlying store. /// /// Under the hood this uses `Key::invalidate` to immediately /// invalidate the key and prevent any further successful /// searches. /// /// Note that the keyutils implementation uses caching, /// and the caches take some time to clear, /// so a key that has been invalidated may still be found /// by get_password if it's called within milliseconds /// in *the same process* that deleted the key. fn delete_credential(&self) -> Result<()> { // Verify that the key exists and is valid let key = self .session .search(&self.description) .map_err(decode_error)?; // Invalidate the key immediately key.invalidate().map_err(decode_error)?; Ok(()) } /// Cast the credential object to std::any::Any. This allows clients /// to downcast the credential to its concrete type so they /// can do platform-specific things with it. fn as_any(&self) -> &dyn std::any::Any { self } /// Expose the concrete debug formatter for use via the [Credential] trait fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } } impl KeyutilsCredential { /// Create a credential from the matching keyutils key. /// /// This is basically a no-op, because keys don't have extra attributes, /// but at least we make sure the underlying platform credential exists. pub fn get_credential(&self) -> Result { self.session .search(&self.description) .map_err(decode_error)?; Ok(self.clone()) } /// Create the platform credential for a Keyutils entry. /// /// An explicit target string is interpreted as the KeyRing to use for the entry. /// If none is provided, then we concatenate the user and service in the string /// `keyring-rs:user@service`. pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result { // Obtain the session keyring let session = KeyRing::from_special_id(KeyRingIdentifier::Session, false).map_err(decode_error)?; // Link the persistent keyring to the session let persistent = KeyRing::get_persistent(KeyRingIdentifier::Session).ok(); // Construct the credential with a URI-style description let description = match target { Some("") => { return Err(ErrorCode::Invalid( "target".to_string(), "cannot be empty".to_string(), )); } Some(value) => value.to_string(), None => format!("keyring-rs:{user}@{service}"), }; Ok(Self { session, persistent, description, }) } } /// The builder for keyutils credentials #[derive(Debug, Copy, Clone)] struct KeyutilsCredentialBuilder {} /// Return a keyutils credential builder. /// /// If features are set to make keyutils the default store, /// this will be automatically be called once before the /// first credential is created. pub fn default_credential_builder() -> Box { Box::new(KeyutilsCredentialBuilder {}) } impl CredentialBuilderApi for KeyutilsCredentialBuilder { /// Build a keyutils credential with the given target, service, and user. /// /// Building a credential does not create a key in the store. /// It's setting a password that does that. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { Ok(Box::new(KeyutilsCredential::new_with_target( target, service, user, )?)) } /// Return an [Any](std::any::Any) reference to the credential builder. fn as_any(&self) -> &dyn std::any::Any { self } /// Since this keystore keeps credentials in kernel memory, /// they vanish on reboot fn persistence(&self) -> CredentialPersistence { CredentialPersistence::UntilReboot } } /// Map an underlying keyutils error to a platform-independent error with annotation. pub fn decode_error(err: KeyError) -> ErrorCode { match err { // Experimentation has shown that the keyutils implementation can return a lot of // different errors that all mean "no such key", depending on where in the invalidation // processing the [get_password](KeyutilsCredential::get_password) call is made. KeyError::KeyDoesNotExist | KeyError::AccessDenied | KeyError::KeyRevoked | KeyError::KeyExpired => ErrorCode::NoEntry, KeyError::InvalidDescription => ErrorCode::Invalid( "description".to_string(), "rejected by platform".to_string(), ), KeyError::InvalidArguments => { ErrorCode::Invalid("password".to_string(), "rejected by platform".to_string()) } other => ErrorCode::PlatformFailure(wrap(other)), } } fn wrap(err: KeyError) -> Box { Box::new(err) } #[cfg(test)] mod tests { use crate::credential::CredentialPersistence; use crate::{tests::generate_random_string, Entry, Error}; use super::{default_credential_builder, KeyutilsCredential}; #[test] fn test_persistence() { assert!(matches!( default_credential_builder().persistence(), CredentialPersistence::UntilReboot )) } fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor(KeyutilsCredential::new_with_target, service, user) } #[test] fn test_invalid_parameter() { let credential = KeyutilsCredential::new_with_target(Some(""), "service", "user"); assert!( matches!(credential, Err(Error::Invalid(_, _))), "Created entry with empty target" ); } #[test] fn test_empty_service_and_user() { crate::tests::test_empty_service_and_user(entry_new); } #[test] fn test_missing_entry() { crate::tests::test_missing_entry(entry_new); } #[test] fn test_empty_password() { let entry = entry_new("empty password service", "empty password user"); assert!( matches!(entry.set_password(""), Err(Error::Invalid(_, _))), "Able to set empty password" ); } #[test] fn test_round_trip_ascii_password() { crate::tests::test_round_trip_ascii_password(entry_new); } #[test] fn test_round_trip_non_ascii_password() { crate::tests::test_round_trip_non_ascii_password(entry_new); } #[test] fn test_round_trip_random_secret() { crate::tests::test_round_trip_random_secret(entry_new); } #[test] fn test_update() { crate::tests::test_update(entry_new); } #[test] fn test_noop_get_update_attributes() { crate::tests::test_noop_get_update_attributes(entry_new); } #[test] fn test_get_credential() { let name = generate_random_string(); let entry = entry_new(&name, &name); let credential: &KeyutilsCredential = entry .get_credential() .downcast_ref() .expect("Not a Keyutils credential"); assert!( credential.get_credential().is_err(), "Platform credential shouldn't exist yet!" ); entry .set_password("test get_credential") .expect("Can't set password for get_credential"); assert!(credential.get_credential().is_ok()); entry .delete_credential() .expect("Couldn't delete after get_credential"); assert!(matches!(entry.get_password(), Err(Error::NoEntry))); } } keyring-3.6.3/src/keyutils_persistent.rs000064400000000000000000000211661046102023000166010ustar 00000000000000/*! # Linux (keyutils) store with Secret Service backing This store, contributed by [@soywod](https://github.com/soywod), uses the [keyutils module](crate::keyutils) as a cache available to headless processes, while using the [secret-service module](crate::secret_service) to provide credential storage beyond reboot. The expected usage pattern for this module is as follows: - Processes that run on headless systems are built with `keyutils` support via the `linux-native` feature of this crate. After each reboot, these processes are either launched after the keyutils cache has been reloaded from the secret service, or (if launched immediately) they wait until the keyutils cache has been reloaded. - A headed "configuration" process is built with this module that allows its user to configure the credentials needed by the headless processes. After each reboot, this process unlocks the secret service (see both the keyutils and secret-service module for information about how this can be done headlessly, if desired) and then accesses each of the configured credentials (which loads them into keyutils). At that point the headless clients can be started (or become active, if already started). This store works by creating a keyutils entry and a secret-service entry for each of its entries. Because keyutils entries don't have attributes, entries in this store don't expose attributes either. Because keyutils entries can't store empty passwords/secrets, this store's entries can't either. See the documentation for the `keyutils` and `secret-service` modules if you want details about how the underlying storage is handled. */ use log::debug; use super::credential::{ Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, }; use super::error::{Error, Result}; use super::keyutils::KeyutilsCredential; use super::secret_service::{SsCredential, SsCredentialBuilder}; /// Representation of a keyutils-persistent credential. /// /// The credential owns a [KeyutilsCredential] for in-memory usage and /// a [SsCredential] for persistence. #[derive(Debug, Clone)] pub struct KeyutilsPersistentCredential { keyutils: KeyutilsCredential, ss: SsCredential, } impl CredentialApi for KeyutilsPersistentCredential { /// Set a password in the underlying store fn set_password(&self, password: &str) -> Result<()> { self.set_secret(password.as_bytes()) } /// Set a secret in the underlying store /// /// It sets first the secret in keyutils, then in /// secret-service. If the latter set fails, the former /// is reverted. fn set_secret(&self, secret: &[u8]) -> Result<()> { let prev_secret = self.keyutils.get_secret(); self.keyutils.set_secret(secret)?; if let Err(err) = self.ss.set_secret(secret) { debug!("Failed set of secret-service: {err}; reverting keyutils"); match prev_secret { Ok(ref secret) => self.keyutils.set_secret(secret), Err(Error::NoEntry) => self.keyutils.delete_credential(), Err(err) => Err(err), }?; return Err(err); } Ok(()) } /// Retrieve a password from the underlying store /// /// The password is retrieved from keyutils. In case of error, the /// password is retrieved from secret-service instead (and /// keyutils is updated). fn get_password(&self) -> Result { match self.keyutils.get_password() { Ok(password) => { return Ok(password); } Err(err) => { debug!("Failed get from keyutils: {err}; trying secret service") } } let password = self.ss.get_password().map_err(ambiguous_to_no_entry)?; self.keyutils.set_password(&password)?; Ok(password) } /// Retrieve a secret from the underlying store /// /// The secret is retrieved from keyutils. In case of error, the /// secret is retrieved from secret-service instead (and keyutils /// is updated). fn get_secret(&self) -> Result> { match self.keyutils.get_secret() { Ok(secret) => { return Ok(secret); } Err(err) => { debug!("Failed get from keyutils: {err}; trying secret service") } } let secret = self.ss.get_secret().map_err(ambiguous_to_no_entry)?; self.keyutils.set_secret(&secret)?; Ok(secret) } /// Delete a password from the underlying store. /// /// The credential is deleted from both keyutils and /// secret-service. fn delete_credential(&self) -> Result<()> { if let Err(err) = self.keyutils.delete_credential() { debug!("cannot delete keyutils credential: {err}"); } self.ss.delete_credential() } fn as_any(&self) -> &dyn std::any::Any { self } fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } } impl KeyutilsPersistentCredential { /// Create the platform credential for a Keyutils entry. /// /// This just passes the arguments to the underlying two stores /// and wraps their results with an entry that holds both. pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result { let ss = SsCredential::new_with_target(target, service, user)?; let keyutils = KeyutilsCredential::new_with_target(target, service, user)?; Ok(Self { keyutils, ss }) } } /// The builder for keyutils-persistent credentials #[derive(Debug, Default)] pub struct KeyutilsPersistentCredentialBuilder {} /// Returns an instance of the keyutils-persistent credential builder. /// /// If keyutils-persistent is the default credential store, this is /// called once when an entry is first created. pub fn default_credential_builder() -> Box { Box::new(KeyutilsPersistentCredentialBuilder {}) } impl CredentialBuilderApi for KeyutilsPersistentCredentialBuilder { /// Build a [KeyutilsPersistentCredential] for the given target, service, and user. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { Ok(Box::new(SsCredential::new_with_target( target, service, user, )?)) } /// Return the underlying builder object with an `Any` type so that it can /// be downgraded to a [KeyutilsPersistentCredentialBuilder] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } /// Return the persistence of this store. /// /// This store's persistence derives from that of the secret service. fn persistence(&self) -> CredentialPersistence { SsCredentialBuilder {}.persistence() } } /// Replace any Ambiguous error with a NoEntry one fn ambiguous_to_no_entry(err: Error) -> Error { if let Error::Ambiguous(_) = err { return Error::NoEntry; }; err } #[cfg(test)] mod tests { use crate::{Entry, Error}; use super::KeyutilsPersistentCredential; fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor( KeyutilsPersistentCredential::new_with_target, service, user, ) } #[test] fn test_invalid_parameter() { let credential = KeyutilsPersistentCredential::new_with_target(Some(""), "service", "user"); assert!( matches!(credential, Err(Error::Invalid(_, _))), "Created entry with empty target" ); } #[test] fn test_empty_service_and_user() { crate::tests::test_empty_service_and_user(entry_new); } #[test] fn test_missing_entry() { crate::tests::test_missing_entry(entry_new); } #[test] fn test_empty_password() { let entry = entry_new("empty password service", "empty password user"); assert!( matches!(entry.set_password(""), Err(Error::Invalid(_, _))), "Able to set empty password" ); } #[test] fn test_round_trip_ascii_password() { crate::tests::test_round_trip_ascii_password(entry_new); } #[test] fn test_round_trip_non_ascii_password() { crate::tests::test_round_trip_non_ascii_password(entry_new); } #[test] fn test_round_trip_random_secret() { crate::tests::test_round_trip_random_secret(entry_new); } #[test] fn test_update() { crate::tests::test_update(entry_new); } #[test] fn test_noop_get_update_attributes() { crate::tests::test_noop_get_update_attributes(entry_new); } } keyring-3.6.3/src/lib.rs000064400000000000000000000722511046102023000132170ustar 00000000000000#![cfg_attr(docsrs, feature(doc_cfg))] /*! # Keyring This is a cross-platform library that does storage and retrieval of passwords (or other secrets) in an underlying platform-specific secure store. A top-level introduction to the library's usage, as well as a small code sample, may be found in [the library's entry on crates.io](https://crates.io/crates/keyring). Currently supported platforms are Linux, FreeBSD, OpenBSD, Windows, macOS, and iOS. ## Design This crate implements a very simple, platform-independent concrete object called an _entry_. Each entry is identified by a <_service name_, _user name_> pair of UTF-8 strings, optionally augmented by a _target_ string (which can be used to distinguish two entries that have the same _service name_ and _user name_). Entries support setting, getting, and forgetting (aka deleting) passwords (UTF-8 strings) and binary secrets (byte arrays). Entries provide persistence for their passwords by wrapping credentials held in platform-specific credential stores. The implementations of these platform-specific stores are captured in two types (with associated traits): - a _credential builder_, represented by the [CredentialBuilder] type (and [CredentialBuilderApi](credential::CredentialBuilderApi) trait). Credential builders are given the identifying information provided for an entry and map it to the identifying information for a platform-specific credential. - a _credential_, represented by the [Credential] type (and [CredentialApi](credential::CredentialApi) trait). The platform-specific credential identified by a builder for an entry is what provides the secure storage for that entry's password/secret. ## Crate-provided Credential Stores 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 provided for an entry are mapped to the service and user attributes that identify a generic credential in the macOS keychain. Typically, platform-specific stores (called _keystores_ in this crate) have a richer model of a credential than the one used by this crate to identify entries. These keystores expose their specific model in the concrete credential objects they use to implement 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 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. 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. - `linux-native-sync-persistent`: Uses both `keyutils` and `sync-secret-service` (see below) for storage. See the docs for the `keyutils_persistent` module for a full explanation of why both are used. Because this store uses the `sync-secret-service`, you can use additional features related to that store (described below). - `linux-native-async-persistent`: Uses both `keyutils` and `async-secret-service` (see below) for storage. See the docs for the `keyutils_persistent` module for a full explanation of why both are used. Because this store uses the `async-secret-service`, you must specify the additional features required by that store (described below). - `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. The Linux platform is the only one for which this crate supplies multiple keystores: native (keyutils), sync or async secret service, and sync or async "combo" (both keyutils and secret service). You cannot specify use of both sync and async keystores; this will lead to a compile error. If you enable a combo keystore on Linux, that will be the default keystore. If you don't enable a combo keystore on Linux, but you do enable both the native and secret service keystores, the secret service will be the default. ## Client-provided Credential Stores In addition to the platform stores implemented by this crate, clients are free to provide their own secure stores and use those. There are two mechanisms provided for this: - Clients can give their desired credential builder to the crate 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 can be identified however clients want, rather than being restricted to the simple model used by this crate. ## Mock Credential Store In addition to the platform-specific credential stores, this crate 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 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, 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. (_N.B._ Since the included credential store implementations are platform-specific, you may need to use the Platform drop-down on [docs.rs](https://docs.rs/keyring) to view the storage module documentation for your desired platform.) ## Caveats This module expects passwords to be UTF-8 encoded strings, so if a third party has stored an arbitrary byte string then retrieving that as a password will return a [BadEncoding](Error::BadEncoding) error. The returned error will have the raw bytes attached, so you can access them, but you can also just fetch them directly using [get_secret](Entry::get_secret) rather than [get_password](Entry::get_password). 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. */ use log::debug; use std::collections::HashMap; pub use credential::{Credential, CredentialBuilder}; pub use error::{Error, Result}; pub mod mock; // // can't use both sync and async secret service // #[cfg(any( all(feature = "sync-secret-service", feature = "async-secret-service"), all( feature = "linux-native-sync-persistent", feature = "linux-native-async-persistent", ) ))] compile_error!("This crate cannot use both the sync and async versions of any credential store"); // // pick the *nix keystore // #[cfg(all(target_os = "linux", feature = "linux-native"))] #[cfg_attr(docsrs, doc(cfg(target_os = "linux")))] pub mod keyutils; #[cfg(all( target_os = "linux", feature = "linux-native", not(feature = "sync-secret-service"), not(feature = "async-secret-service"), ))] pub use keyutils as default; #[cfg(all( any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"), any(feature = "sync-secret-service", feature = "async-secret-service"), ))] #[cfg_attr( docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))) )] pub mod secret_service; #[cfg(all( any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"), any(feature = "sync-secret-service", feature = "async-secret-service"), not(any( feature = "linux-native-sync-persistent", feature = "linux-native-async-persistent", )), ))] pub use secret_service as default; #[cfg(all( target_os = "linux", any( feature = "linux-native-sync-persistent", feature = "linux-native-async-persistent", ) ))] #[cfg_attr(docsrs, doc(cfg(target_os = "linux")))] pub mod keyutils_persistent; #[cfg(all( target_os = "linux", any( feature = "linux-native-sync-persistent", feature = "linux-native-async-persistent", ), ))] pub use keyutils_persistent as default; // fallback to mock if neither keyutils nor secret service is available #[cfg(any( all( target_os = "linux", not(feature = "linux-native"), not(feature = "sync-secret-service"), not(feature = "async-secret-service"), ), all( any(target_os = "freebsd", target_os = "openbsd"), not(feature = "sync-secret-service"), not(feature = "async-secret-service"), ) ))] pub use mock as default; // // pick the Apple keystore // #[cfg(all(target_os = "macos", feature = "apple-native"))] #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] pub mod macos; #[cfg(all(target_os = "macos", feature = "apple-native"))] pub use macos as default; #[cfg(all(target_os = "macos", not(feature = "apple-native")))] pub use mock as default; #[cfg(all(target_os = "ios", feature = "apple-native"))] #[cfg_attr(docsrs, doc(cfg(target_os = "ios")))] pub mod ios; #[cfg(all(target_os = "ios", feature = "apple-native"))] pub use ios as default; #[cfg(all(target_os = "ios", not(feature = "apple-native")))] pub use mock as default; // // pick the Windows keystore // #[cfg(all(target_os = "windows", feature = "windows-native"))] #[cfg_attr(docsrs, doc(cfg(target_os = "windows")))] pub mod windows; #[cfg(all(target_os = "windows", not(feature = "windows-native")))] pub use mock as default; #[cfg(all(target_os = "windows", feature = "windows-native"))] pub use windows as default; #[cfg(not(any( target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "macos", target_os = "ios", target_os = "windows", )))] pub use mock as default; pub mod credential; pub mod error; #[derive(Default, Debug)] struct EntryBuilder { inner: Option>, } static DEFAULT_BUILDER: std::sync::RwLock = std::sync::RwLock::new(EntryBuilder { inner: None }); /// Set the credential builder used by default to create entries. /// /// 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 /// stores and want precise control over which credential is in which store, /// then use [new_with_credential](Entry::new_with_credential). /// /// This will block waiting for all other threads currently creating entries /// to complete what they are doing. It's really meant to be called /// at app startup before you start creating entries. pub fn set_default_credential_builder(new: Box) { let mut guard = DEFAULT_BUILDER .write() .expect("Poisoned RwLock in keyring-rs: please report a bug!"); guard.inner = Some(new); } fn build_default_credential(target: Option<&str>, service: &str, user: &str) -> Result { 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 = 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 }) } #[derive(Debug)] pub struct Entry { inner: Box, } impl Entry { /// Create an entry for the given service and user. /// /// The default credential builder is used. /// /// # Errors /// /// 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. /// /// # Panics /// /// 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 /// details (and preferably a backtrace) so the developers can investigate. pub fn new(service: &str, user: &str) -> Result { debug!("creating entry with service {service}, user {user}, and no target"); let entry = build_default_credential(None, service, user)?; debug!("created entry {:?}", entry.inner); Ok(entry) } /// Create an entry for the given target, service, and user. /// /// The default credential builder is used. pub fn new_with_target(target: &str, service: &str, user: &str) -> Result { debug!("creating entry with service {service}, user {user}, and target {target}"); let entry = build_default_credential(Some(target), service, user)?; debug!("created entry {:?}", entry.inner); Ok(entry) } /// Create an entry that uses the given platform credential for storage. pub fn new_with_credential(credential: Box) -> Entry { debug!("create entry from {credential:?}"); Entry { inner: credential } } /// Set the password for this entry. /// /// Can return an [Ambiguous](Error::Ambiguous) error /// if there is more than one platform credential /// that matches this entry. This can only happen /// on some platforms, and then only if a third-party /// application wrote the ambiguous credential. pub fn set_password(&self, password: &str) -> Result<()> { debug!("set password for entry {:?}", self.inner); self.inner.set_password(password) } /// Set the secret for this entry. /// /// Can return an [Ambiguous](Error::Ambiguous) error /// if there is more than one platform credential /// that matches this entry. This can only happen /// on some platforms, and then only if a third-party /// application wrote the ambiguous credential. pub fn set_secret(&self, secret: &[u8]) -> Result<()> { debug!("set secret for entry {:?}", self.inner); self.inner.set_secret(secret) } /// Retrieve the password saved for this entry. /// /// Returns a [NoEntry](Error::NoEntry) error if there isn't one. /// /// Can return an [Ambiguous](Error::Ambiguous) error /// if there is more than one platform credential /// that matches this entry. This can only happen /// on some platforms, and then only if a third-party /// application wrote the ambiguous credential. pub fn get_password(&self) -> Result { debug!("get password from entry {:?}", self.inner); self.inner.get_password() } /// Retrieve the secret saved for this entry. /// /// Returns a [NoEntry](Error::NoEntry) error if there isn't one. /// /// Can return an [Ambiguous](Error::Ambiguous) error /// if there is more than one platform credential /// that matches this entry. This can only happen /// on some platforms, and then only if a third-party /// application wrote the ambiguous credential. pub fn get_secret(&self) -> Result> { debug!("get secret from entry {:?}", self.inner); self.inner.get_secret() } /// Get the attributes on the underlying credential for this entry. /// /// Some of the underlying credential stores allow credentials to have named attributes /// 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. /// /// Returns a [NoEntry](Error::NoEntry) error if there isn't a credential for this entry. /// /// Can return an [Ambiguous](Error::Ambiguous) error /// if there is more than one platform credential /// that matches this entry. This can only happen /// on some platforms, and then only if a third-party /// application wrote the ambiguous credential. pub fn get_attributes(&self) -> Result> { debug!("get attributes from entry {:?}", self.inner); self.inner.get_attributes() } /// Update the attributes on the underlying credential for this entry. /// /// Some of the underlying credential stores allow credentials to have named attributes /// that can be set to string values. See the documentation for each credential store /// for a list of which attribute names can be given values by this call. To support /// cross-platform use, each credential store ignores (without error) any specified attributes /// that aren't supported by that store. /// /// Returns a [NoEntry](Error::NoEntry) error if there isn't a credential for this entry. /// /// Can return an [Ambiguous](Error::Ambiguous) error /// if there is more than one platform credential /// that matches this entry. This can only happen /// on some platforms, and then only if a third-party /// application wrote the ambiguous credential. pub fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> { debug!( "update attributes for entry {:?} from map {attributes:?}", self.inner ); self.inner.update_attributes(attributes) } /// Delete the underlying credential for this entry. /// /// Returns a [NoEntry](Error::NoEntry) error if there isn't one. /// /// Can return an [Ambiguous](Error::Ambiguous) error /// if there is more than one platform credential /// that matches this entry. This can only happen /// on some platforms, and then only if a third-party /// application wrote the ambiguous credential. /// /// Note: This does _not_ affect the lifetime of the [Entry] /// structure, which is controlled by Rust. It only /// affects the underlying credential store. pub fn delete_credential(&self) -> Result<()> { debug!("delete entry {:?}", self.inner); self.inner.delete_credential() } /// Return a reference to this entry's wrapped credential. /// /// 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 { self.inner.as_any() } } #[cfg(doctest)] doc_comment::doctest!("../README.md", readme); #[cfg(test)] /// There are no actual tests in this module. /// Instead, it contains generics that each keystore invokes in their tests, /// 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 { use super::{credential::CredentialApi, Entry, Error, Result}; use std::collections::HashMap; /// Create a platform-specific credential given the constructor, service, and user pub fn entry_from_constructor(f: F, service: &str, user: &str) -> Entry where F: FnOnce(Option<&str>, &str, &str) -> Result, T: 'static + CredentialApi + Send + Sync, { match f(None, service, user) { Ok(credential) => Entry::new_with_credential(Box::new(credential)), Err(err) => { panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}") } } } /// Create a platform-specific credential given the constructor, service, user, and attributes pub fn entry_from_constructor_and_attributes( f: F, service: &str, user: &str, attrs: &HashMap<&str, &str>, ) -> Entry where F: FnOnce(Option<&str>, &str, &str, &HashMap<&str, &str>) -> Result, 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:?}") } } } /// A basic round-trip unit test given an entry and a password. pub fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) { entry .set_password(in_pass) .unwrap_or_else(|err| panic!("Can't set password for {case}: {err:?}")); let out_pass = entry .get_password() .unwrap_or_else(|err| panic!("Can't get password for {case}: {err:?}")); assert_eq!( in_pass, out_pass, "Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'", ); entry .delete_credential() .unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}")); let password = entry.get_password(); assert!( matches!(password, Err(Error::NoEntry)), "Read deleted password for {case}", ); } /// A basic round-trip unit test given an entry and a password. pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) { entry .set_secret(in_secret) .unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}")); let out_secret = entry .get_secret() .unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}")); assert_eq!( in_secret, &out_secret, "Passwords don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'", ); entry .delete_credential() .unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}")); let password = entry.get_secret(); assert!( matches!(password, Err(Error::NoEntry)), "Read deleted password for {case}", ); } /// When tests fail, they leave keys behind, and those keys /// have to be cleaned up before the tests can be run again /// in order to avoid bad results. So it's a lot easier just /// to have tests use a random string for key names to avoid /// the conflicts, and then do any needed cleanup once everything /// is working correctly. So we export this function for tests to use. pub fn generate_random_string_of_len(len: usize) -> String { use fastrand; use std::iter::repeat_with; repeat_with(fastrand::alphanumeric).take(len).collect() } pub fn generate_random_string() -> String { generate_random_string_of_len(30) } fn generate_random_bytes_of_len(len: usize) -> Vec { use fastrand; use std::iter::repeat_with; repeat_with(|| fastrand::u8(..)).take(len).collect() } pub fn test_empty_service_and_user(f: F) where F: Fn(&str, &str) -> Entry, { let name = generate_random_string(); let in_pass = "doesn't matter"; test_round_trip("empty user", &f(&name, ""), in_pass); test_round_trip("empty service", &f("", &name), in_pass); test_round_trip("empty service & user", &f("", ""), in_pass); } pub fn test_missing_entry(f: F) where F: FnOnce(&str, &str) -> Entry, { let name = generate_random_string(); let entry = f(&name, &name); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Missing entry has password" ) } pub fn test_empty_password(f: F) where F: FnOnce(&str, &str) -> Entry, { let name = generate_random_string(); let entry = f(&name, &name); test_round_trip("empty password", &entry, ""); } pub fn test_round_trip_ascii_password(f: F) where F: FnOnce(&str, &str) -> Entry, { let name = generate_random_string(); let entry = f(&name, &name); test_round_trip("ascii password", &entry, "test ascii password"); } pub fn test_round_trip_non_ascii_password(f: F) where F: FnOnce(&str, &str) -> Entry, { let name = generate_random_string(); let entry = f(&name, &name); test_round_trip("non-ascii password", &entry, "このきれいな花は桜です"); } pub fn test_round_trip_random_secret(f: F) where F: FnOnce(&str, &str) -> Entry, { let name = generate_random_string(); let entry = f(&name, &name); let secret = generate_random_bytes_of_len(24); test_round_trip_secret("non-ascii password", &entry, secret.as_slice()); } pub fn test_update(f: F) where F: FnOnce(&str, &str) -> Entry, { let name = generate_random_string(); let entry = f(&name, &name); test_round_trip("initial ascii password", &entry, "test ascii password"); test_round_trip( "updated non-ascii password", &entry, "このきれいな花は桜です", ); } pub fn test_noop_get_update_attributes(f: F) where F: FnOnce(&str, &str) -> Entry, { let name = generate_random_string(); let entry = f(&name, &name); assert!( matches!(entry.get_attributes(), Err(Error::NoEntry)), "Read missing credential in attribute test", ); let mut map: HashMap<&str, &str> = HashMap::new(); map.insert("test attribute name", "test attribute value"); assert!( matches!(entry.update_attributes(&map), Err(Error::NoEntry)), "Updated missing credential in attribute test", ); // create the credential and test again entry .set_password("test password for attributes") .unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}")); match entry.get_attributes() { Err(err) => panic!("Couldn't get attributes: {err:?}"), Ok(attrs) if attrs.is_empty() => {} Ok(attrs) => panic!("Unexpected attributes: {attrs:?}"), } assert!( matches!(entry.update_attributes(&map), Ok(())), "Couldn't update attributes in attribute test", ); match entry.get_attributes() { Err(err) => panic!("Couldn't get attributes after update: {err:?}"), Ok(attrs) if attrs.is_empty() => {} Ok(attrs) => panic!("Unexpected attributes after update: {attrs:?}"), } entry .delete_credential() .unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}")); assert!( matches!(entry.get_attributes(), Err(Error::NoEntry)), "Read deleted credential in attribute test", ); } } keyring-3.6.3/src/macos.rs000064400000000000000000000317401046102023000135510ustar 00000000000000/*! # macOS Keychain credential store All credentials on macOS are stored in secure stores called _keychains_. The OS automatically creates three of them (or four if removable media is being used), called _User_ (aka login), _Common_, _System_, and _Dynamic_. The target attribute of an [Entry](crate::Entry) determines (case-insensitive) 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) one of the four built-in keychains, the 'User' keychain is used. For a given service/user pair, this module creates/searches for a credential in the target keychain whose _account_ attribute holds the user and whose _name_ attribute holds the service. Because of a quirk in the Mac keychain services API, neither the _account_ nor the _name_ may be the empty string. (Empty strings are treated as wildcards when looking up credentials by attribute value.) In the _Keychain Access_ UI on Mac, credentials created by this module show up in the passwords area (with their _where_ field equal to their _name_). What the Keychain Access lists under _Note_ entries on the Mac are also generic credentials, so existing _notes_ created by third-party applications can be accessed by this module if you know the value of their _account_ attribute (which is not displayed by _Keychain Access_). Credentials on macOS can have a large number of _key/value_ attributes, but this module controls the _account_ and _name_ attributes and ignores all the others. so clients can't use it to access or update any attributes. */ use security_framework::base::Error; use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain}; use security_framework::os::macos::passwords::find_generic_password; use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; use super::error::{decode_password, Error as ErrorCode, Result}; /// The representation of a generic Keychain credential. /// /// The actual credentials can have lots of attributes /// not represented here. There's no way to use this /// module to get at those attributes. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacCredential { pub domain: MacKeychainDomain, pub service: String, pub account: String, } impl CredentialApi for MacCredential { /// Create and write a credential with password for this entry. /// /// The new credential replaces any existing one in the store. /// Since there is only one credential with a given _account_ and _user_ /// in any given keychain, there is no chance of ambiguity. fn set_password(&self, password: &str) -> Result<()> { get_keychain(self)? .set_generic_password(&self.service, &self.account, password.as_bytes()) .map_err(decode_error)?; Ok(()) } /// Create and write a credential with secret for this entry. /// /// The new credential replaces any existing one in the store. /// Since there is only one credential with a given _account_ and _user_ /// in any given keychain, there is no chance of ambiguity. fn set_secret(&self, secret: &[u8]) -> Result<()> { get_keychain(self)? .set_generic_password(&self.service, &self.account, secret) .map_err(decode_error)?; Ok(()) } /// Look up the password for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn get_password(&self) -> Result { let (password_bytes, _) = find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account) .map_err(decode_error)?; decode_password(password_bytes.to_owned()) } /// Look up the secret for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn get_secret(&self) -> Result> { let (password_bytes, _) = find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account) .map_err(decode_error)?; Ok(password_bytes.to_owned()) } /// Delete the underlying generic credential for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn delete_credential(&self) -> Result<()> { let (_, item) = find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account) .map_err(decode_error)?; item.delete(); Ok(()) } /// Return the underlying concrete object with an `Any` type so that it can /// be downgraded to a [MacCredential] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } /// Expose the concrete debug formatter for use via the [Credential] trait fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } } impl MacCredential { /// Construct a credential from the underlying generic credential. /// /// On Mac, this is basically a no-op, because we represent any attributes /// other than the ones we use to find the generic credential. /// But at least this checks whether the underlying credential exists. pub fn get_credential(&self) -> Result { let (_, _) = find_generic_password(Some(&[get_keychain(self)?]), &self.service, &self.account) .map_err(decode_error)?; Ok(self.clone()) } /// Create a credential representing a Mac keychain entry. /// /// A target string is interpreted as the keychain to use for the entry. /// /// Creating a credential does not put anything into the keychain. /// The keychain entry will be created /// when [set_password](MacCredential::set_password) is /// called. /// /// This will fail if the service or user strings are empty, /// because empty attribute values act as wildcards in the /// Keychain Services API. pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result { if service.is_empty() { return Err(ErrorCode::Invalid( "service".to_string(), "cannot be empty".to_string(), )); } if user.is_empty() { return Err(ErrorCode::Invalid( "user".to_string(), "cannot be empty".to_string(), )); } let domain = if let Some(target) = target { target.parse()? } else { MacKeychainDomain::User }; Ok(Self { domain, service: service.to_string(), account: user.to_string(), }) } } /// The builder for Mac keychain credentials pub struct MacCredentialBuilder {} /// Returns an instance of the Mac credential builder. /// /// On Mac, /// this is called once when an entry is first created. pub fn default_credential_builder() -> Box { Box::new(MacCredentialBuilder {}) } impl CredentialBuilderApi for MacCredentialBuilder { /// Build a [MacCredential] for the given target, service, and user. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { Ok(Box::new(MacCredential::new_with_target( target, service, user, )?)) } /// Return the underlying builder object with an `Any` type so that it can /// be downgraded to a [MacCredentialBuilder] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } } #[derive(Debug, Clone, PartialEq, Eq)] /// The four pre-defined Mac keychains. pub enum MacKeychainDomain { User, System, Common, Dynamic, } impl std::fmt::Display for MacKeychainDomain { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MacKeychainDomain::User => "User".fmt(f), MacKeychainDomain::System => "System".fmt(f), MacKeychainDomain::Common => "Common".fmt(f), MacKeychainDomain::Dynamic => "Dynamic".fmt(f), } } } impl std::str::FromStr for MacKeychainDomain { type Err = ErrorCode; /// Convert a target specification string to a keychain domain. /// /// We accept any case in the string, /// but the value has to match a known keychain domain name /// or else we assume the login keychain is meant. fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { "user" => Ok(MacKeychainDomain::User), "system" => Ok(MacKeychainDomain::System), "common" => Ok(MacKeychainDomain::Common), "dynamic" => Ok(MacKeychainDomain::Dynamic), _ => Err(ErrorCode::Invalid( "target".to_string(), format!("'{s}' is not User, System, Common, or Dynamic"), )), } } } fn get_keychain(cred: &MacCredential) -> Result { let domain = match cred.domain { MacKeychainDomain::User => SecPreferencesDomain::User, MacKeychainDomain::System => SecPreferencesDomain::System, MacKeychainDomain::Common => SecPreferencesDomain::Common, MacKeychainDomain::Dynamic => SecPreferencesDomain::Dynamic, }; match SecKeychain::default_for_domain(domain) { Ok(keychain) => Ok(keychain), Err(err) => Err(decode_error(err)), } } /// Map a Mac API error to a crate error with appropriate annotation /// /// The macOS error code values used here are from /// [this reference](https://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-78/lib/SecBase.h.auto.html) pub fn decode_error(err: Error) -> ErrorCode { match err.code() { -25291 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecNotAvailable -25292 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecReadOnly -25294 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecNoSuchKeychain -25295 => ErrorCode::NoStorageAccess(Box::new(err)), // errSecInvalidKeychain -25300 => ErrorCode::NoEntry, // errSecItemNotFound _ => ErrorCode::PlatformFailure(Box::new(err)), } } #[cfg(test)] mod tests { use crate::credential::CredentialPersistence; use crate::{tests::generate_random_string, Entry, Error}; use super::{default_credential_builder, MacCredential}; #[test] fn test_persistence() { assert!(matches!( default_credential_builder().persistence(), CredentialPersistence::UntilDelete )) } fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor(MacCredential::new_with_target, service, user) } #[test] fn test_invalid_parameter() { let credential = MacCredential::new_with_target(None, "", "user"); assert!( matches!(credential, Err(Error::Invalid(_, _))), "Created credential with empty service" ); let credential = MacCredential::new_with_target(None, "service", ""); assert!( matches!(credential, Err(Error::Invalid(_, _))), "Created entry with empty user" ); let credential = MacCredential::new_with_target(Some(""), "service", "user"); assert!( matches!(credential, Err(Error::Invalid(_, _))), "Created entry with empty target" ); } #[test] fn test_missing_entry() { crate::tests::test_missing_entry(entry_new); } #[test] fn test_empty_password() { crate::tests::test_empty_password(entry_new); } #[test] fn test_round_trip_ascii_password() { crate::tests::test_round_trip_ascii_password(entry_new); } #[test] fn test_round_trip_non_ascii_password() { crate::tests::test_round_trip_non_ascii_password(entry_new); } #[test] fn test_round_trip_random_secret() { crate::tests::test_round_trip_random_secret(entry_new); } #[test] fn test_update() { crate::tests::test_update(entry_new); } #[test] fn test_get_credential() { let name = generate_random_string(); let entry = entry_new(&name, &name); let credential: &MacCredential = entry .get_credential() .downcast_ref() .expect("Not a mac credential"); assert!( credential.get_credential().is_err(), "Platform credential shouldn't exist yet!" ); entry .set_password("test get_credential") .expect("Can't set password for get_credential"); assert!(credential.get_credential().is_ok()); entry .delete_credential() .expect("Couldn't delete after get_credential"); assert!(matches!(entry.get_password(), Err(Error::NoEntry))); } #[test] fn test_get_update_attributes() { crate::tests::test_noop_get_update_attributes(entry_new); } } keyring-3.6.3/src/mock.rs000064400000000000000000000256141046102023000134030ustar 00000000000000/*! # Mock credential store To facilitate testing of clients, this crate provides a Mock credential store that is platform-independent, provides no persistence, and allows the client to specify the return values (including errors) for each call. The credentials in this store have no attributes at all. To use this credential store instead of the default, make this call during application startup _before_ creating any entries: ```rust # use keyring::{set_default_credential_builder, mock}; set_default_credential_builder(mock::default_credential_builder()); ``` 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 in the entry itself, so getting a password before setting it will always result in a [NoEntry](Error::NoEntry) error. 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) 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 call on the mock will operate as usual. Here's a complete example: ```rust # use keyring::{Entry, Error, mock, mock::MockCredential}; # keyring::set_default_credential_builder(mock::default_credential_builder()); let entry = Entry::new("service", "user").unwrap(); let mock: &MockCredential = entry.get_credential().downcast_ref().unwrap(); mock.set_error(Error::Invalid("mock error".to_string(), "takes precedence".to_string())); entry.set_password("test").expect_err("error will override"); entry.set_password("test").expect("error has been cleared"); ``` */ use std::cell::RefCell; use std::sync::Mutex; use super::credential::{ Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi, CredentialPersistence, }; use super::error::{decode_password, Error, Result}; /// The concrete mock credential /// /// Mocks use an internal mutability pattern since entries are read-only. /// The mutex is used to make sure these are Sync. #[derive(Debug)] pub struct MockCredential { pub inner: Mutex>, } impl Default for MockCredential { fn default() -> Self { Self { inner: Mutex::new(RefCell::new(Default::default())), } } } /// The (in-memory) persisted data for a mock credential. /// /// We keep a password, but unlike most keystores /// we also keep an intended error to return on the next call. /// /// (Everything about this structure is public for transparency. /// Most keystore implementation hide their internals.) #[derive(Debug, Default)] pub struct MockData { pub secret: Option>, pub error: Option, } impl CredentialApi for MockCredential { /// Set a password on a mock credential. /// /// If there is an error in the mock, it will be returned /// and the password will _not_ be set. The error will /// be cleared, so calling again will set the password. fn set_password(&self, password: &str) -> Result<()> { let mut inner = self.inner.lock().expect("Can't access mock data for set"); let data = inner.get_mut(); let err = data.error.take(); match err { None => { data.secret = Some(password.as_bytes().to_vec()); Ok(()) } Some(err) => Err(err), } } /// Set a password on a mock credential. /// /// If there is an error in the mock, it will be returned /// and the password will _not_ be set. The error will /// be cleared, so calling again will set the password. fn set_secret(&self, secret: &[u8]) -> Result<()> { let mut inner = self.inner.lock().expect("Can't access mock data for set"); let data = inner.get_mut(); let err = data.error.take(); match err { None => { data.secret = Some(secret.to_vec()); Ok(()) } Some(err) => Err(err), } } /// Get the password from a mock credential, if any. /// /// If there is an error set in the mock, it will /// be returned instead of a password. fn get_password(&self) -> Result { let mut inner = self.inner.lock().expect("Can't access mock data for get"); let data = inner.get_mut(); let err = data.error.take(); match err { None => match &data.secret { None => Err(Error::NoEntry), Some(val) => decode_password(val.clone()), }, Some(err) => Err(err), } } /// Get the password from a mock credential, if any. /// /// If there is an error set in the mock, it will /// be returned instead of a password. fn get_secret(&self) -> Result> { let mut inner = self.inner.lock().expect("Can't access mock data for get"); let data = inner.get_mut(); let err = data.error.take(); match err { None => match &data.secret { None => Err(Error::NoEntry), Some(val) => Ok(val.clone()), }, Some(err) => Err(err), } } /// Delete the password in a mock credential /// /// If there is an error, it will be returned and /// the deletion will not happen. /// /// If there is no password, a [NoEntry](Error::NoEntry) error /// will be returned. fn delete_credential(&self) -> Result<()> { let mut inner = self .inner .lock() .expect("Can't access mock data for delete"); let data = inner.get_mut(); let err = data.error.take(); match err { None => match data.secret { Some(_) => { data.secret = None; Ok(()) } None => Err(Error::NoEntry), }, Some(err) => Err(err), } } /// Return this mock credential concrete object /// wrapped in the [Any](std::any::Any) trait, /// so it can be downcast. fn as_any(&self) -> &dyn std::any::Any { self } /// Expose the concrete debug formatter for use via the [Credential] trait fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } } impl MockCredential { /// Make a new mock credential. /// /// Since mocks have no persistence between sessions, /// new mocks always have no password. fn new_with_target(_target: Option<&str>, _service: &str, _user: &str) -> Result { Ok(Default::default()) } /// Set an error to be returned from this mock credential. /// /// Error returns always take precedence over the normal /// behavior of the mock. But once an error has been /// returned it is removed, so the mock works thereafter. pub fn set_error(&self, err: Error) { let mut inner = self .inner .lock() .expect("Can't access mock data for set_error"); let data = inner.get_mut(); data.error = Some(err); } } /// The builder for mock credentials. pub struct MockCredentialBuilder {} impl CredentialBuilderApi for MockCredentialBuilder { /// Build a mock credential for the given target, service, and user. /// /// Since mocks don't persist between sessions, all mocks /// start off without passwords. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { let credential = MockCredential::new_with_target(target, service, user)?; Ok(Box::new(credential)) } /// Get an [Any][std::any::Any] reference to the mock credential builder. fn as_any(&self) -> &dyn std::any::Any { self } /// This keystore keeps the password in the entry! fn persistence(&self) -> CredentialPersistence { CredentialPersistence::EntryOnly } } /// Return a mock credential builder for use by clients. pub fn default_credential_builder() -> Box { Box::new(MockCredentialBuilder {}) } #[cfg(test)] mod tests { use super::{default_credential_builder, MockCredential}; use crate::credential::CredentialPersistence; use crate::{tests::generate_random_string, Entry, Error}; #[test] fn test_persistence() { assert!(matches!( default_credential_builder().persistence(), CredentialPersistence::EntryOnly )) } fn entry_new(service: &str, user: &str) -> Entry { let credential = MockCredential::new_with_target(None, service, user).unwrap(); Entry::new_with_credential(Box::new(credential)) } #[test] fn test_missing_entry() { crate::tests::test_missing_entry(entry_new); } #[test] fn test_empty_password() { crate::tests::test_empty_password(entry_new); } #[test] fn test_round_trip_ascii_password() { crate::tests::test_round_trip_ascii_password(entry_new); } #[test] fn test_round_trip_non_ascii_password() { crate::tests::test_round_trip_non_ascii_password(entry_new); } #[test] fn test_round_trip_random_secret() { crate::tests::test_round_trip_random_secret(entry_new); } #[test] fn test_update() { crate::tests::test_update(entry_new); } #[test] fn test_get_update_attributes() { crate::tests::test_noop_get_update_attributes(entry_new); } #[test] fn test_set_error() { let name = generate_random_string(); let entry = entry_new(&name, &name); let password = "test ascii password"; let mock: &MockCredential = entry .inner .as_any() .downcast_ref() .expect("Downcast failed"); mock.set_error(Error::Invalid( "mock error".to_string(), "is an error".to_string(), )); assert!( matches!(entry.set_password(password), Err(Error::Invalid(_, _))), "set: No error" ); entry .set_password(password) .expect("set: Error not cleared"); mock.set_error(Error::NoEntry); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "get: No error" ); let stored_password = entry.get_password().expect("get: Error not cleared"); assert_eq!( stored_password, password, "Retrieved and set ascii passwords don't match" ); mock.set_error(Error::TooLong("mock".to_string(), 3)); assert!( matches!(entry.delete_credential(), Err(Error::TooLong(_, 3))), "delete: No error" ); entry .delete_credential() .expect("delete: Error not cleared"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted ascii password" ) } } keyring-3.6.3/src/secret_service.rs000064400000000000000000001040761046102023000154570ustar 00000000000000/*! # secret-service credential store Items in the secret-service are identified by an arbitrary collection of attributes. This implementation controls the following attributes: - `target` (optional & taken from entry creation call, defaults to `default`) - `service` (required & taken from entry creation call) - `username` (required & taken from entry creation call's `user` parameter) In addition, when creating a new credential, this implementation assigns two additional attributes: - `application` (set to `rust-keyring-client`) - `label` (set to a string with the user, service, target, and keyring version at time of creation) Client code is allowed to retrieve and to set all attributes _except_ the three that are controlled by this implementation. (N.B. The `label` string is not actually an attribute; it's a required element in every item and is used by GUI tools as the name for the item. But this implementation treats the label as if it were any other non-controlled attribute, with the caveat that it will reject any attempt to set the label to an empty string.) Existing items are always searched for at the service level, which means all collections are searched. The search attributes used are `target` (set from the entry target), `service` (set from the entry service), and `username` (set from the entry user). Because earlier versions of this crate did not set the `target` attribute on credentials that were stored in the default collection, a fallback search is done for items in the default collection with no `target` attribute *if the original search for all three attributes returns no matches*. New items are created in the default collection, unless a target other than `default` is specified for the entry, in which case the item will be created in a collection (created if necessary) that is labeled with the specified target. Setting the password on an entry will always update the password on an existing item in preference to creating a new item. This provides better compatibility with 3rd party clients, as well as earlier versions of this crate, that may already have created items that match the entry, and thus reduces the chance of ambiguity in later searches. ## Tokio runtime caution If you are using the `async-secret-service` with this crate, and specifying `tokio` as your runtime, be careful: if you make keyring calls on the main thread, you will likely deadlock (see\ [this issue on GitHub](https://github.com/hwchen/keyring-rs/issues/132) for details). You need to spawn a separate thread on which you make your keyring calls to avoid this. ## Headless usage If you must use the secret-service on a headless linux box, 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/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"). The following `bash` function may be helpful: ```shell function unlock-keyring () { read -rsp "Password: " pass echo -n "$pass" | gnome-keyring-daemon --unlock unset pass } ``` For an excellent treatment of all the headless dbus issues, see [this answer on ServerFault](https://serverfault.com/a/906224/79617). ## Usage - not! - on Windows Subsystem for Linux As noted in [this issue on GitHub](https://github.com/hwchen/keyring-rs/issues/133), there is no "default" collection defined under WSL. So this keystore doesn't work "out of the box" on WSL. See the issue for more details and possible workarounds. */ use std::collections::HashMap; #[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}; /// The representation of an item in the secret-service. /// /// This structure has two roles. On the one hand, it captures all the /// information a user specifies for an [Entry](crate::Entry) /// and so is the basis for our search /// (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 /// has the same attributes and label as the item. #[derive(Debug, Clone)] pub struct SsCredential { pub attributes: HashMap, pub label: String, target: Option, } impl CredentialApi for SsCredential { /// Sets the password on a unique matching item, if it exists, or creates one if necessary. /// /// If there are multiple matches, /// returns an [Ambiguous](ErrorCode::Ambiguous) error with a credential for each /// matching item. /// /// When creating, the item is put into a collection named by the credential's `target` /// attribute. fn set_password(&self, password: &str) -> Result<()> { self.set_secret(password.as_bytes()) } /// Sets the secret on a unique matching item, if it exists, or creates one if necessary. /// /// If there are multiple matches, /// returns an [Ambiguous](ErrorCode::Ambiguous) error with a credential for each /// matching item. /// /// When creating, the item is put into a collection named by the credential's `target` /// attribute. fn set_secret(&self, secret: &[u8]) -> Result<()> { #[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_secret(i, secret), true) { Ok(_) => return Ok(()), Err(ErrorCode::NoEntry) => {} Err(err) => return Err(err), } // 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 // the [new] or [new_with_target] commands will have explicit targets. But entries // created to wrap 3rd-party items that don't have `target` attributes may not. let name = self.target.as_ref().ok_or_else(empty_target)?; let collection = get_collection(&ss, name).or_else(|_| create_collection(&ss, name))?; collection .create_item( self.label.as_str(), self.all_attributes(), secret, true, // replace "text/plain", ) .map_err(platform_failure)?; Ok(()) } /// Gets the password on a unique matching item, if it exists. /// /// If there are no /// matching items, returns a [NoEntry](ErrorCode::NoEntry) error. /// If there are multiple matches, /// returns an [Ambiguous](ErrorCode::Ambiguous) /// error with a credential for each matching item. fn get_password(&self) -> Result { Ok(self.map_matching_items(get_item_password, true)?.remove(0)) } /// Gets the secret on a unique matching item, if it exists. /// /// If there are no /// matching items, returns a [NoEntry](ErrorCode::NoEntry) error. /// If there are multiple matches, /// returns an [Ambiguous](ErrorCode::Ambiguous) /// error with a credential for each matching item. fn get_secret(&self) -> Result> { Ok(self.map_matching_items(get_item_secret, true)?.remove(0)) } /// Get attributes on a unique matching item, if it exists fn get_attributes(&self) -> Result> { let attributes: Vec> = self.map_matching_items(get_item_attributes, true)?; Ok(attributes.into_iter().next().unwrap()) } /// Update attributes on a unique matching item, if it exists fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> { self.map_matching_items(|i| update_item_attributes(i, attributes), true)?; Ok(()) } /// Deletes the unique matching item, if it exists. /// /// If there are no /// matching items, returns a [NoEntry](ErrorCode::NoEntry) error. /// If there are multiple matches, /// returns an [Ambiguous](ErrorCode::Ambiguous) /// error with a credential for each matching item. fn delete_credential(&self) -> Result<()> { self.map_matching_items(delete_item, true)?; Ok(()) } /// Return the underlying credential object with an `Any` type so that it can /// be downgraded to an [SsCredential] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } /// Expose the concrete debug formatter for use via the [Credential] trait fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } } impl SsCredential { /// Create a credential for the given target, service, and user. /// /// The target defaults to `default` (the default secret-service collection). /// /// Creating this credential does not create a matching item. /// If there isn't already one there, it will be created only /// when [set_password](SsCredential::set_password) is /// called. pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result { if let Some("") = target { return Err(empty_target()); } let target = target.unwrap_or("default"); let attributes = HashMap::from([ ("service".to_string(), service.to_string()), ("username".to_string(), user.to_string()), ("target".to_string(), target.to_string()), ("application".to_string(), "rust-keyring".to_string()), ]); Ok(Self { attributes, label: format!( "{user}@{service}:{target} (keyring v{})", env!("CARGO_PKG_VERSION"), ), target: Some(target.to_string()), }) } /// Create a credential that has *no* target and the given service and user. /// /// This emulates what keyring v1 did, and can be very handy when you need to /// access an old v1 credential that's in your secret service default collection. pub fn new_with_no_target(service: &str, user: &str) -> Result { let attributes = HashMap::from([ ("service".to_string(), service.to_string()), ("username".to_string(), user.to_string()), ("application".to_string(), "rust-keyring".to_string()), ]); Ok(Self { attributes, label: format!( "keyring-rs v{} for no target, service '{service}', user '{user}'", env!("CARGO_PKG_VERSION"), ), target: None, }) } /// Create a credential from an underlying item. /// /// The created credential will have all the attributes and label /// of the underlying item, so you can examine them. pub fn new_from_item(item: &Item) -> Result { let attributes = item.get_attributes().map_err(decode_error)?; let target = attributes.get("target").cloned(); Ok(Self { attributes, label: item.get_label().map_err(decode_error)?, target, }) } /// Construct a credential for this credential's underlying matching item, /// if there is exactly one. pub fn new_from_matching_item(&self) -> Result { Ok(self .map_matching_items(Self::new_from_item, true)? .remove(0)) } /// If there are multiple matching items for this credential, get all of their passwords. /// /// (This is useful if [get_password](SsCredential::get_password) /// returns an [Ambiguous](ErrorCode::Ambiguous) error.) pub fn get_all_passwords(&self) -> Result> { self.map_matching_items(get_item_password, false) } /// If there are multiple matching items for this credential, delete all of them. /// /// (This is useful if [delete_credential](SsCredential::delete_credential) /// returns an [Ambiguous](ErrorCode::Ambiguous) error.) pub fn delete_all_passwords(&self) -> Result<()> { self.map_matching_items(delete_item, false)?; Ok(()) } /// Map a function over the items matching this credential. /// /// Items are unlocked before the function is applied. /// /// If `require_unique` is true, and there are no matching items, then /// a [NoEntry](ErrorCode::NoEntry) error is returned. /// If `require_unique` is true, and there are multiple matches, /// then an [Ambiguous](ErrorCode::Ambiguous) error is returned /// with a vector containing one /// credential for each of the matching items. pub fn map_matching_items(&self, f: F, require_unique: bool) -> Result> where F: Fn(&Item) -> Result, T: Sized, { #[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(false).into_iter().collect(); let search = ss.search_items(attributes).map_err(decode_error)?; let count = search.locked.len() + search.unlocked.len(); if count == 0 { if let Some("default") = self.target.as_deref() { return self.map_matching_legacy_items(&ss, f, require_unique); } } if require_unique { if count == 0 { return Err(ErrorCode::NoEntry); } else if count > 1 { let mut creds: Vec> = vec![]; for item in search.locked.iter().chain(search.unlocked.iter()) { let cred = Self::new_from_item(item)?; creds.push(Box::new(cred)) } return Err(ErrorCode::Ambiguous(creds)); } } let mut results: Vec = vec![]; for item in search.unlocked.iter() { results.push(f(item)?); } for item in search.locked.iter() { item.unlock().map_err(decode_error)?; results.push(f(item)?); } Ok(results) } /// Map a function over items that older versions of keyring /// would have matched against this credential. /// /// Keyring v1 created secret service items that had no target attribute, and it was /// only able to create items in the default collection. Keyring v2, and Keyring v3.1, /// in order to be able to find items set by keyring v1, would first look for items /// everywhere independent of target attribute, and then filter those found by the value /// of the target attribute. But this matching behavior overgeneralized when the keyring /// was locked at the time of the search (see /// [issue #204](https://github.com/hwchen/keyring-rs/issues/204) for details). /// /// As of keyring v3.2, the service-wide search behavior was changed to require a /// matching target on items. But, as pointed out in /// [issue #207](https://github.com/hwchen/keyring-rs/issues/207), /// this meant that items set by keyring v1 (or by 3rd party tools that didn't set /// the target attribute) would not be found, even if they were in the default /// collection. /// /// So with keyring v3.2.1, if the service-wide search fails to find any matching /// credential, and the credential being searched for has the default target (or /// no target), we fall back and search the default collection for a v1-style credential. /// That preserves the legacy behavior at the cost of a second round-trip through /// the secret service for the collection search. pub fn map_matching_legacy_items( &self, ss: &SecretService, f: F, require_unique: bool, ) -> Result> where F: Fn(&Item) -> Result, T: Sized, { let collection = ss.get_default_collection().map_err(decode_error)?; let attributes = self.search_attributes(true); let search = collection.search_items(attributes).map_err(decode_error)?; if require_unique { if search.is_empty() && require_unique { return Err(ErrorCode::NoEntry); } else if search.len() > 1 { let mut creds: Vec> = vec![]; for item in search.iter() { let cred = Self::new_from_item(item)?; creds.push(Box::new(cred)) } return Err(ErrorCode::Ambiguous(creds)); } } let mut results: Vec = vec![]; for item in search.iter() { results.push(f(item)?); } Ok(results) } /// Using strings in the credential map makes managing the lifetime /// of the credential much easier. But since the secret service expects /// a map from &str to &str, we have this utility to transform the /// credential's map into one of the right form. fn all_attributes(&self) -> HashMap<&str, &str> { self.attributes .iter() .map(|(k, v)| (k.as_str(), v.as_str())) .collect() } /// Similar to [all_attributes](SsCredential::all_attributes), /// but this just selects the ones we search on fn search_attributes(&self, omit_target: bool) -> HashMap<&str, &str> { let mut result: HashMap<&str, &str> = HashMap::new(); if self.target.is_some() && !omit_target { result.insert("target", self.attributes["target"].as_str()); } result.insert("service", self.attributes["service"].as_str()); result.insert("username", self.attributes["username"].as_str()); result } } /// The builder for secret-service credentials #[derive(Debug, Default)] pub struct SsCredentialBuilder {} /// Returns an instance of the secret-service credential builder. /// /// If secret-service is the default credential store, /// this is called once when an entry is first created. pub fn default_credential_builder() -> Box { Box::new(SsCredentialBuilder {}) } impl CredentialBuilderApi for SsCredentialBuilder { /// Build an [SsCredential] for the given target, service, and user. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { Ok(Box::new(SsCredential::new_with_target( target, service, user, )?)) } /// Return the underlying builder object with an `Any` type so that it can /// be downgraded to an [SsCredentialBuilder] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } } // // Secret Service utilities // /// Find the secret service collection whose label is the given name. /// /// The name `default` is treated specially and is interpreted as naming /// the default collection regardless of its label (which might be different). pub fn get_collection<'a>(ss: &'a SecretService, name: &str) -> Result> { let collection = if name.eq("default") { ss.get_default_collection().map_err(decode_error)? } else { let all = ss.get_all_collections().map_err(decode_error)?; let found = all .into_iter() .find(|c| c.get_label().map(|l| l.eq(name)).unwrap_or(false)); found.ok_or(ErrorCode::NoEntry)? }; if collection.is_locked().map_err(decode_error)? { collection.unlock().map_err(decode_error)?; } Ok(collection) } /// Create a secret service collection labeled with the given name. /// /// If a collection with that name already exists, it is returned. /// /// The name `default` is specially interpreted to mean the default collection. pub fn create_collection<'a>(ss: &'a SecretService, name: &str) -> Result> { let collection = if name.eq("default") { ss.get_default_collection().map_err(decode_error)? } else { ss.create_collection(name, "").map_err(decode_error)? }; Ok(collection) } /// Given an existing item, set its secret. pub fn set_item_secret(item: &Item, secret: &[u8]) -> Result<()> { item.set_secret(secret, "text/plain").map_err(decode_error) } /// Given an existing item, retrieve and decode its password. pub fn get_item_password(item: &Item) -> Result { let bytes = item.get_secret().map_err(decode_error)?; decode_password(bytes) } /// Given an existing item, retrieve its secret. pub fn get_item_secret(item: &Item) -> Result> { let secret = item.get_secret().map_err(decode_error)?; Ok(secret) } /// Given an existing item, retrieve its non-controlled attributes. pub fn get_item_attributes(item: &Item) -> Result> { let mut attributes = item.get_attributes().map_err(decode_error)?; attributes.remove("target"); attributes.remove("service"); attributes.remove("username"); attributes.insert("label".to_string(), item.get_label().map_err(decode_error)?); Ok(attributes) } /// Given an existing item, retrieve its non-controlled attributes. pub fn update_item_attributes(item: &Item, attributes: &HashMap<&str, &str>) -> Result<()> { let existing = item.get_attributes().map_err(decode_error)?; let mut updated: HashMap<&str, &str> = HashMap::new(); for (k, v) in existing.iter() { updated.insert(k, v); } for (k, v) in attributes.iter() { if k.eq(&"target") || k.eq(&"service") || k.eq(&"username") { continue; } if k.eq(&"label") { if v.is_empty() { return Err(ErrorCode::Invalid( "label".to_string(), "cannot be empty".to_string(), )); } item.set_label(v).map_err(decode_error)?; if updated.contains_key("label") { updated.insert("label", v); } } else { updated.insert(k, v); } } item.set_attributes(updated).map_err(decode_error)?; Ok(()) } // Given an existing item, delete it. pub fn delete_item(item: &Item) -> Result<()> { item.delete().map_err(decode_error) } // // Error utilities // /// Map underlying secret-service errors to crate errors with /// appropriate annotation. pub fn decode_error(err: Error) -> ErrorCode { match err { Error::Locked => no_access(err), Error::NoResult => no_access(err), Error::Prompt => no_access(err), _ => platform_failure(err), } } fn empty_target() -> ErrorCode { ErrorCode::Invalid("target".to_string(), "cannot be empty".to_string()) } fn platform_failure(err: Error) -> ErrorCode { ErrorCode::PlatformFailure(wrap(err)) } fn no_access(err: Error) -> ErrorCode { ErrorCode::NoStorageAccess(wrap(err)) } fn wrap(err: Error) -> Box { Box::new(err) } #[cfg(test)] mod tests { use crate::credential::CredentialPersistence; use crate::{tests::generate_random_string, Entry, Error}; use std::collections::HashMap; use super::{default_credential_builder, SsCredential}; #[test] fn test_persistence() { assert!(matches!( default_credential_builder().persistence(), CredentialPersistence::UntilDelete )) } fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor(SsCredential::new_with_target, service, user) } #[test] fn test_invalid_parameter() { let credential = SsCredential::new_with_target(Some(""), "service", "user"); assert!( matches!(credential, Err(Error::Invalid(_, _))), "Created entry with empty target" ); } #[test] fn test_empty_service_and_user() { crate::tests::test_empty_service_and_user(entry_new); } #[test] fn test_missing_entry() { crate::tests::test_missing_entry(entry_new); } #[test] fn test_empty_password() { crate::tests::test_empty_password(entry_new); } #[test] fn test_round_trip_ascii_password() { crate::tests::test_round_trip_ascii_password(entry_new); } #[test] fn test_round_trip_non_ascii_password() { crate::tests::test_round_trip_non_ascii_password(entry_new); } #[test] fn test_round_trip_random_secret() { crate::tests::test_round_trip_random_secret(entry_new); } #[test] fn test_update() { crate::tests::test_update(entry_new); } #[test] fn test_get_credential() { let name = generate_random_string(); let entry = entry_new(&name, &name); entry .set_password("test get credential") .expect("Can't set password for get_credential"); let credential: &SsCredential = entry .get_credential() .downcast_ref() .expect("Not a secret service credential"); let actual = credential .new_from_matching_item() .expect("Can't read credential"); assert_eq!(actual.label, credential.label, "Labels don't match"); for (key, value) in &credential.attributes { assert_eq!( actual.attributes.get(key).expect("Missing attribute"), value, "Attribute mismatch" ) } entry .delete_credential() .expect("Couldn't delete get-credential"); assert!(matches!(entry.get_password(), Err(Error::NoEntry))); } #[test] fn test_get_update_attributes() { let name = generate_random_string(); let credential = SsCredential::new_with_target(None, &name, &name) .expect("Can't create credential for attribute test"); let create_label = credential.label.clone(); let entry = Entry::new_with_credential(Box::new(credential)); assert!( matches!(entry.get_attributes(), Err(Error::NoEntry)), "Read missing credential in attribute test", ); let mut in_map: HashMap<&str, &str> = HashMap::new(); in_map.insert("label", "test label value"); in_map.insert("test attribute name", "test attribute value"); in_map.insert("target", "ignored target value"); in_map.insert("service", "ignored service value"); in_map.insert("username", "ignored username value"); assert!( matches!(entry.update_attributes(&in_map), Err(Error::NoEntry)), "Updated missing credential in attribute test", ); // create the credential and test again entry .set_password("test password for attributes") .unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}")); let out_map = entry .get_attributes() .expect("Can't get attributes after create"); assert_eq!(out_map["label"], create_label); assert_eq!(out_map["application"], "rust-keyring"); assert!(!out_map.contains_key("target")); assert!(!out_map.contains_key("service")); assert!(!out_map.contains_key("username")); assert!( matches!(entry.update_attributes(&in_map), Ok(())), "Couldn't update attributes in attribute test", ); let after_map = entry .get_attributes() .expect("Can't get attributes after update"); assert_eq!(after_map["label"], in_map["label"]); assert_eq!( after_map["test attribute name"], in_map["test attribute name"] ); assert_eq!(out_map["application"], "rust-keyring"); in_map.insert("label", ""); assert!( matches!(entry.update_attributes(&in_map), Err(Error::Invalid(_, _))), "Was able to set empty label in attribute test", ); entry .delete_credential() .unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}")); assert!( matches!(entry.get_attributes(), Err(Error::NoEntry)), "Read deleted credential in attribute test", ); } #[test] #[ignore = "can't be run headless, because it needs to prompt"] fn test_create_new_target_collection() { let name = generate_random_string(); let credential = SsCredential::new_with_target(Some(&name), &name, &name) .expect("Can't create credential for new collection"); let entry = Entry::new_with_credential(Box::new(credential)); let password = "password in new collection"; entry .set_password(password) .expect("Can't set password for new collection entry"); let actual = entry .get_password() .expect("Can't get password for new collection entry"); assert_eq!(actual, password); entry .delete_credential() .expect("Couldn't delete password for new collection entry"); assert!(matches!(entry.get_password(), Err(Error::NoEntry))); delete_collection(&name); } #[test] #[ignore = "can't be run headless, because it needs to prompt"] fn test_separate_targets_dont_interfere() { let name1 = generate_random_string(); let name2 = generate_random_string(); let credential1 = SsCredential::new_with_target(Some(&name1), &name1, &name1) .expect("Can't create credential1 with new collection"); let entry1 = Entry::new_with_credential(Box::new(credential1)); let credential2 = SsCredential::new_with_target(Some(&name2), &name1, &name1) .expect("Can't create credential2 with new collection"); let entry2 = Entry::new_with_credential(Box::new(credential2)); let entry3 = Entry::new(&name1, &name1).expect("Can't create entry in default collection"); let password1 = "password for collection 1"; let password2 = "password for collection 2"; let password3 = "password for default collection"; entry1 .set_password(password1) .expect("Can't set password for collection 1"); entry2 .set_password(password2) .expect("Can't set password for collection 2"); entry3 .set_password(password3) .expect("Can't set password for default collection"); let actual1 = entry1 .get_password() .expect("Can't get password for collection 1"); assert_eq!(actual1, password1); let actual2 = entry2 .get_password() .expect("Can't get password for collection 2"); assert_eq!(actual2, password2); let actual3 = entry3 .get_password() .expect("Can't get password for default collection"); assert_eq!(actual3, password3); entry1 .delete_credential() .expect("Couldn't delete password for collection 1"); assert!(matches!(entry1.get_password(), Err(Error::NoEntry))); entry2 .delete_credential() .expect("Couldn't delete password for collection 2"); assert!(matches!(entry2.get_password(), Err(Error::NoEntry))); entry3 .delete_credential() .expect("Couldn't delete password for default collection"); assert!(matches!(entry3.get_password(), Err(Error::NoEntry))); delete_collection(&name1); delete_collection(&name2); } #[test] fn test_legacy_entry() { let name = generate_random_string(); let pw = "test password"; let v3_entry = Entry::new(&name, &name).expect("Can't create v3 entry"); let _ = v3_entry.get_password().expect_err("Found v3 entry"); create_v1_entry(&name, pw); let password = v3_entry.get_password().expect("Can't find v1 entry"); assert_eq!(password, pw); v3_entry.delete_credential().expect("Can't delete v1 entry"); let _ = v3_entry .get_password() .expect_err("Got password for v1 entry after delete"); } fn delete_collection(name: &str) { #[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::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"); } fn create_v1_entry(name: &str, password: &str) { #[cfg(not(feature = "async-secret-service"))] use dbus_secret_service::{EncryptionType, SecretService}; #[cfg(feature = "async-secret-service")] use secret_service::{blocking::SecretService, EncryptionType}; let cred = SsCredential::new_with_no_target(name, name) .expect("Can't create credential with no target"); let ss = SecretService::connect(EncryptionType::Plain).expect("Can't connect to secret service"); let collection = ss .get_default_collection() .expect("Can't get default collection"); collection .create_item( cred.label.as_str(), cred.all_attributes(), password.as_bytes(), true, // replace "text/plain", ) .expect("Can't create item with no target in default collection"); } } keyring-3.6.3/src/windows.rs000064400000000000000000000740401046102023000141410ustar 00000000000000/*! # Windows Credential Manager credential store This module uses Windows Generic credentials to store entries. These are identified by a single string (called their _target name_). They also have a number of non-identifying but manipulable attributes: a _username_, a _comment_, and a _target alias_. For a given <_service_, _username_> pair, this module uses the concatenated string `username.service` as the mapped credential's _target name_, and fills the _username_ and _comment_ fields with appropriate strings. (This convention allows multiple users to store passwords for the same service.) Because the Windows credential manager doesn't support multiple collections of credentials, and because many Windows programs use _only_ the service name as the credential _target name_, the `Entry::new_with_target` call uses the `target` parameter as the credential's _target name_ rather than concatenating the username and service. So if you have a custom algorithm you want to use for computing the Windows target name, 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.) The [get_attributes](crate::Entry::get_attributes) call will return the values in the `username`, `comment`, and `target_alias` fields (using those strings as the attribute names), and the [update_attributes](crate::Entry::update_attributes) call allows setting those fields. ## Caveat Reads and writes of the same entry from multiple threads are not guaranteed to be serialized by the Windows Credential Manager in the order in which they were made. Careful testing has shown that modifying the same entry in the same (almost simultaneous) order from different threads produces different results on different runs. */ use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; use super::error::{Error as ErrorCode, Result}; use byteorder::{ByteOrder, LittleEndian}; use std::collections::HashMap; use std::iter::once; use std::mem::MaybeUninit; use std::str; use windows_sys::Win32::Foundation::{ GetLastError, ERROR_BAD_USERNAME, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NOT_FOUND, ERROR_NO_SUCH_LOGON_SESSION, FILETIME, }; use windows_sys::Win32::Security::Credentials::{ CredDeleteW, CredFree, CredReadW, CredWriteW, CREDENTIALW, CREDENTIAL_ATTRIBUTEW, CRED_FLAGS, CRED_MAX_CREDENTIAL_BLOB_SIZE, CRED_MAX_GENERIC_TARGET_NAME_LENGTH, CRED_MAX_STRING_LENGTH, CRED_MAX_USERNAME_LENGTH, CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC, }; use zeroize::Zeroize; /// The representation of a Windows Generic credential. /// /// See the module header for the meanings of these fields. #[derive(Debug, Clone, PartialEq, Eq)] pub struct WinCredential { pub username: String, pub target_name: String, pub target_alias: String, pub comment: String, } // Windows API type mappings: // DWORD is u32 // LPCWSTR is *const u16 // BOOL is i32 (false = 0, true = 1) // PCREDENTIALW = *mut CREDENTIALW impl CredentialApi for WinCredential { /// Create and write a credential with password for this entry. /// /// The new credential replaces any existing one in the store. /// Since there is only one credential with a given _target name_, /// there is no chance of ambiguity. fn set_password(&self, password: &str) -> Result<()> { self.validate_attributes(None, Some(password))?; // Password strings are converted to UTF-16, because that's the native // charset for Windows strings. This allows interoperability with native // Windows credential APIs. But the storage for the credential is actually // a little-endian blob, because Windows credentials can contain anything. let mut blob_u16 = to_wstr_no_null(password); let mut blob = vec![0; blob_u16.len() * 2]; LittleEndian::write_u16_into(&blob_u16, &mut blob); let result = self.set_secret(&blob); // make sure that the copies of the secret are erased blob_u16.zeroize(); blob.zeroize(); result } /// Create and write a credential with secret for this entry. /// /// The new credential replaces any existing one in the store. /// Since there is only one credential with a given _target name_, /// there is no chance of ambiguity. fn set_secret(&self, secret: &[u8]) -> Result<()> { self.validate_attributes(Some(secret), None)?; self.save_credential(secret) } /// Look up the password for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn get_password(&self) -> Result { self.extract_from_platform(extract_password) } /// Look up the secret for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn get_secret(&self) -> Result> { self.extract_from_platform(extract_secret) } /// Get the attributes from the credential for this entry, if it exists. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn get_attributes(&self) -> Result> { let cred = self.extract_from_platform(Self::extract_credential)?; let mut attributes: HashMap = HashMap::new(); attributes.insert("comment".to_string(), cred.comment.clone()); attributes.insert("target_alias".to_string(), cred.target_alias.clone()); attributes.insert("username".to_string(), cred.username.clone()); Ok(attributes) } /// Update the attributes on the credential for this entry, if it exists. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn update_attributes(&self, attributes: &HashMap<&str, &str>) -> Result<()> { let secret = self.extract_from_platform(extract_secret)?; let mut cred = self.extract_from_platform(Self::extract_credential)?; if let Some(comment) = attributes.get(&"comment") { cred.comment = comment.to_string(); } if let Some(target_alias) = attributes.get(&"target_alias") { cred.target_alias = target_alias.to_string(); } if let Some(username) = attributes.get(&"username") { cred.username = username.to_string(); } cred.validate_attributes(Some(&secret), None)?; cred.save_credential(&secret) } /// Delete the underlying generic credential for this entry, if any. /// /// Returns a [NoEntry](ErrorCode::NoEntry) error if there is no /// credential in the store. fn delete_credential(&self) -> Result<()> { self.validate_attributes(None, None)?; let target_name = to_wstr(&self.target_name); let cred_type = CRED_TYPE_GENERIC; match unsafe { CredDeleteW(target_name.as_ptr(), cred_type, 0) } { 0 => Err(decode_error()), _ => Ok(()), } } /// Return the underlying concrete object with an `Any` type so that it can /// be downgraded to a [WinCredential] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } /// Expose the concrete debug formatter for use via the [Credential] trait fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } } impl WinCredential { fn validate_attributes(&self, secret: Option<&[u8]>, password: Option<&str>) -> Result<()> { if self.username.len() > CRED_MAX_USERNAME_LENGTH as usize { return Err(ErrorCode::TooLong( String::from("user"), CRED_MAX_USERNAME_LENGTH, )); } if self.target_name.is_empty() { return Err(ErrorCode::Invalid( "target".to_string(), "cannot be empty".to_string(), )); } if self.target_name.len() > CRED_MAX_GENERIC_TARGET_NAME_LENGTH as usize { return Err(ErrorCode::TooLong( String::from("target"), CRED_MAX_GENERIC_TARGET_NAME_LENGTH, )); } if self.target_alias.len() > CRED_MAX_STRING_LENGTH as usize { return Err(ErrorCode::TooLong( String::from("target alias"), CRED_MAX_STRING_LENGTH, )); } if self.comment.len() > CRED_MAX_STRING_LENGTH as usize { return Err(ErrorCode::TooLong( String::from("comment"), CRED_MAX_STRING_LENGTH, )); } if let Some(secret) = secret { if secret.len() > CRED_MAX_CREDENTIAL_BLOB_SIZE as usize { return Err(ErrorCode::TooLong( String::from("secret"), CRED_MAX_CREDENTIAL_BLOB_SIZE, )); } } if let Some(password) = password { // We're going to store the password as UTF-16, so first transform it to UTF-16, // count its runes, and then multiply by 2 to get the number of bytes needed. if password.encode_utf16().count() * 2 > CRED_MAX_CREDENTIAL_BLOB_SIZE as usize { return Err(ErrorCode::TooLong( String::from("password encoded as UTF-16"), CRED_MAX_CREDENTIAL_BLOB_SIZE, )); } } Ok(()) } /// Write this credential into the underlying store as a Generic credential /// /// You must always have validated attributes before you call this! fn save_credential(&self, secret: &[u8]) -> Result<()> { let mut username = to_wstr(&self.username); let mut target_name = to_wstr(&self.target_name); let mut target_alias = to_wstr(&self.target_alias); let mut comment = to_wstr(&self.comment); let mut blob = secret.to_vec(); let blob_len = blob.len() as u32; let flags = CRED_FLAGS::default(); let cred_type = CRED_TYPE_GENERIC; let persist = CRED_PERSIST_ENTERPRISE; // Ignored by CredWriteW let last_written = FILETIME { dwLowDateTime: 0, dwHighDateTime: 0, }; let attribute_count = 0; let attributes: *mut CREDENTIAL_ATTRIBUTEW = std::ptr::null_mut(); let mut credential = CREDENTIALW { Flags: flags, Type: cred_type, TargetName: target_name.as_mut_ptr(), Comment: comment.as_mut_ptr(), LastWritten: last_written, CredentialBlobSize: blob_len, CredentialBlob: blob.as_mut_ptr(), Persist: persist, AttributeCount: attribute_count, Attributes: attributes, TargetAlias: target_alias.as_mut_ptr(), UserName: username.as_mut_ptr(), }; // raw pointer to credential, is coerced from &mut let p_credential: *const CREDENTIALW = &mut credential; // Call windows API let result = match unsafe { CredWriteW(p_credential, 0) } { 0 => Err(decode_error()), _ => Ok(()), }; // erase the copy of the secret blob.zeroize(); result } /// Construct a credential from this credential's underlying Generic credential. /// /// This can be useful for seeing modifications made by a third party. pub fn get_credential(&self) -> Result { self.extract_from_platform(Self::extract_credential) } fn extract_from_platform(&self, f: F) -> Result where F: FnOnce(&CREDENTIALW) -> Result, { self.validate_attributes(None, None)?; let mut p_credential = MaybeUninit::uninit(); // at this point, p_credential is just a pointer to nowhere. // The allocation happens in the `CredReadW` call below. let result = { let cred_type = CRED_TYPE_GENERIC; let target_name = to_wstr(&self.target_name); unsafe { CredReadW( target_name.as_ptr(), cred_type, 0, p_credential.as_mut_ptr(), ) } }; match result { 0 => { // `CredReadW` failed, so no allocation has been done, so no free needs to be done Err(decode_error()) } _ => { // `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: // first we remove the "uninitialized" guard around it, then we reinterpret it as a // pointer to the right structure type. let p_credential = unsafe { p_credential.assume_init() }; let w_credential: CREDENTIALW = unsafe { *p_credential }; // Now we can apply the passed extractor function to the credential. let result = f(&w_credential); // Finally, we erase the secret and free the allocated credential. erase_secret(&w_credential); unsafe { CredFree(p_credential as *mut _) }; result } } } fn extract_credential(w_credential: &CREDENTIALW) -> Result { Ok(Self { username: unsafe { from_wstr(w_credential.UserName) }, target_name: unsafe { from_wstr(w_credential.TargetName) }, target_alias: unsafe { from_wstr(w_credential.TargetAlias) }, comment: unsafe { from_wstr(w_credential.Comment) }, }) } /// Create a credential for the given target, service, and user. /// /// Creating a credential does not create a matching Generic credential /// in the Windows Credential Manager. /// If there isn't already one there, it will be created only /// when [set_password](WinCredential::set_password) is /// called. pub fn new_with_target( target: Option<&str>, service: &str, user: &str, ) -> Result { const VERSION: &str = env!("CARGO_PKG_VERSION"); let credential = if let Some(target) = target { // if target.is_empty() { // return Err(ErrorCode::Invalid( // "target".to_string(), // "cannot be empty".to_string(), // )); // } Self { // On Windows, the target name is all that's used to // search for the credential, so we allow clients to // specify it if they want a different convention. username: user.to_string(), target_name: target.to_string(), target_alias: String::new(), comment: format!("keyring v{VERSION}"), } } else { Self { // Note: default concatenation of user and service name is // used because windows uses target_name as sole identifier. // See the module docs for more rationale. Also see this issue // for Python: https://github.com/jaraco/keyring/issues/47 // // Note that it's OK to have an empty user or service name, // because the format for the target name will not be empty. // But it's certainly not recommended. username: user.to_string(), target_name: format!("{user}.{service}"), target_alias: String::new(), comment: format!("keyring v{VERSION}"), } }; credential.validate_attributes(None, None)?; Ok(credential) } } /// The builder for Windows Generic credentials. pub struct WinCredentialBuilder {} /// Returns an instance of the Windows credential builder. /// /// On Windows, /// this is called once when an entry is first created. pub fn default_credential_builder() -> Box { Box::new(WinCredentialBuilder {}) } impl CredentialBuilderApi for WinCredentialBuilder { /// Build a [WinCredential] for the given target, service, and user. fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result> { Ok(Box::new(WinCredential::new_with_target( target, service, user, )?)) } /// Return the underlying builder object with an `Any` type so that it can /// be downgraded to a [WinCredentialBuilder] for platform-specific processing. fn as_any(&self) -> &dyn std::any::Any { self } } fn extract_password(credential: &CREDENTIALW) -> Result { let mut blob = extract_secret(credential)?; // 3rd parties may write credential data with an odd number of bytes, // so we make sure that we don't try to decode those as utf16 if blob.len() % 2 != 0 { return Err(ErrorCode::BadEncoding(blob)); } // This should be a UTF-16 string, so convert it to // a UTF-16 vector and then try to decode it. let mut blob_u16 = vec![0; blob.len() / 2]; LittleEndian::read_u16_into(&blob, &mut blob_u16); let result = match String::from_utf16(&blob_u16) { Err(_) => Err(ErrorCode::BadEncoding(blob)), Ok(s) => { // we aren't returning the blob, so clear it blob.zeroize(); Ok(s) } }; // we aren't returning the utf16 blob, so clear it blob_u16.zeroize(); result } fn extract_secret(credential: &CREDENTIALW) -> Result> { let blob_pointer: *const u8 = credential.CredentialBlob; let blob_len: usize = credential.CredentialBlobSize as usize; if blob_len == 0 { return Ok(Vec::new()); } let blob = unsafe { std::slice::from_raw_parts(blob_pointer, blob_len) }; Ok(blob.to_vec()) } fn erase_secret(credential: &CREDENTIALW) { let blob_pointer: *mut u8 = credential.CredentialBlob; let blob_len: usize = credential.CredentialBlobSize as usize; if blob_len == 0 { return; } let blob = unsafe { std::slice::from_raw_parts_mut(blob_pointer, blob_len) }; blob.zeroize(); } fn to_wstr(s: &str) -> Vec { s.encode_utf16().chain(once(0)).collect() } fn to_wstr_no_null(s: &str) -> Vec { s.encode_utf16().collect() } unsafe fn from_wstr(ws: *const u16) -> String { // null pointer case, return empty string if ws.is_null() { return String::new(); } // this code from https://stackoverflow.com/a/48587463/558006 let len = (0..).take_while(|&i| *ws.offset(i) != 0).count(); if len == 0 { return String::new(); } let slice = std::slice::from_raw_parts(ws, len); String::from_utf16_lossy(slice) } /// Windows error codes are `DWORDS` which are 32-bit unsigned ints. #[derive(Debug)] pub struct Error(pub u32); impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self.0 { ERROR_NO_SUCH_LOGON_SESSION => write!(f, "Windows ERROR_NO_SUCH_LOGON_SESSION"), ERROR_NOT_FOUND => write!(f, "Windows ERROR_NOT_FOUND"), ERROR_BAD_USERNAME => write!(f, "Windows ERROR_BAD_USERNAME"), ERROR_INVALID_FLAGS => write!(f, "Windows ERROR_INVALID_FLAGS"), ERROR_INVALID_PARAMETER => write!(f, "Windows ERROR_INVALID_PARAMETER"), err => write!(f, "Windows error code {err}"), } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } /// Map the last encountered Windows API error to a crate error with appropriate annotation. pub fn decode_error() -> ErrorCode { match unsafe { GetLastError() } { ERROR_NOT_FOUND => ErrorCode::NoEntry, ERROR_NO_SUCH_LOGON_SESSION => { ErrorCode::NoStorageAccess(wrap(ERROR_NO_SUCH_LOGON_SESSION)) } err => ErrorCode::PlatformFailure(wrap(err)), } } fn wrap(code: u32) -> Box { Box::new(Error(code)) } #[cfg(test)] mod tests { use super::*; use crate::credential::CredentialPersistence; use crate::tests::{generate_random_string, generate_random_string_of_len}; use crate::Entry; #[test] fn test_persistence() { assert!(matches!( default_credential_builder().persistence(), CredentialPersistence::UntilDelete )) } fn entry_new(service: &str, user: &str) -> Entry { crate::tests::entry_from_constructor(WinCredential::new_with_target, service, user) } #[test] fn test_bad_password() { fn make_platform_credential(password: &mut Vec) -> CREDENTIALW { let last_written = FILETIME { dwLowDateTime: 0, dwHighDateTime: 0, }; let attribute_count = 0; let attributes: *mut CREDENTIAL_ATTRIBUTEW = std::ptr::null_mut(); CREDENTIALW { Flags: 0, Type: CRED_TYPE_GENERIC, TargetName: std::ptr::null_mut(), Comment: std::ptr::null_mut(), LastWritten: last_written, CredentialBlobSize: password.len() as u32, CredentialBlob: password.as_mut_ptr(), Persist: CRED_PERSIST_ENTERPRISE, AttributeCount: attribute_count, Attributes: attributes, TargetAlias: std::ptr::null_mut(), UserName: std::ptr::null_mut(), } } // the first malformed sequence can't be UTF-16 because it has an odd number of bytes. // the second malformed sequence has a first surrogate marker (0xd800) without a matching // companion (it's taken from the String::fromUTF16 docs). let mut odd_bytes = b"1".to_vec(); let malformed_utf16 = [0xD834, 0xDD1E, 0x006d, 0x0075, 0xD800, 0x0069, 0x0063]; let mut malformed_bytes: Vec = vec![0; malformed_utf16.len() * 2]; LittleEndian::write_u16_into(&malformed_utf16, &mut malformed_bytes); for bytes in [&mut odd_bytes, &mut malformed_bytes] { let credential = make_platform_credential(bytes); match extract_password(&credential) { Err(ErrorCode::BadEncoding(str)) => assert_eq!(&str, bytes), Err(other) => panic!("Bad password ({bytes:?}) decode gave wrong error: {other}"), Ok(s) => panic!("Bad password ({bytes:?}) decode gave results: {s:?}"), } } } #[test] fn test_validate_attributes() { fn validate_attribute_too_long(result: Result<()>, attr: &str, len: u32) { match result { Err(ErrorCode::TooLong(arg, val)) => { if attr == "password" { assert_eq!( &arg, "password encoded as UTF-16", "Error names wrong attribute" ); } else { assert_eq!(&arg, attr, "Error names wrong attribute"); } assert_eq!(val, len, "Error names wrong limit"); } Err(other) => panic!("Error is not '{attr} too long': {other}"), Ok(_) => panic!("No error when {attr} too long"), } } let cred = WinCredential { username: "username".to_string(), target_name: "target_name".to_string(), target_alias: "target_alias".to_string(), comment: "comment".to_string(), }; for (attr, len) in [ ("user", CRED_MAX_USERNAME_LENGTH), ("target", CRED_MAX_GENERIC_TARGET_NAME_LENGTH), ("target alias", CRED_MAX_STRING_LENGTH), ("comment", CRED_MAX_STRING_LENGTH), ("password", CRED_MAX_CREDENTIAL_BLOB_SIZE), ("secret", CRED_MAX_CREDENTIAL_BLOB_SIZE), ] { let long_string = generate_random_string_of_len(1 + len as usize); let mut bad_cred = cred.clone(); match attr { "user" => bad_cred.username = long_string.clone(), "target" => bad_cred.target_name = long_string.clone(), "target alias" => bad_cred.target_alias = long_string.clone(), "comment" => bad_cred.comment = long_string.clone(), _ => (), } let validate = |r| validate_attribute_too_long(r, attr, len); match attr { "password" => { let password = generate_random_string_of_len((len / 2) as usize + 1); validate(bad_cred.validate_attributes(None, Some(&password))) } "secret" => { let secret: Vec = vec![255u8; len as usize + 1]; validate(bad_cred.validate_attributes(Some(&secret), None)) } _ => validate(bad_cred.validate_attributes(None, None)), } } } #[test] fn test_password_valid_only_after_conversion_to_utf16() { let cred = WinCredential { username: "username".to_string(), target_name: "target_name".to_string(), target_alias: "target_alias".to_string(), comment: "comment".to_string(), }; let len = CRED_MAX_CREDENTIAL_BLOB_SIZE / 2; let password: String = (0..len).map(|_| "笑").collect(); assert!(password.len() > CRED_MAX_CREDENTIAL_BLOB_SIZE as usize); cred.validate_attributes(None, Some(&password)) .expect("Password of appropriate length in UTF16 was invalid"); } #[test] fn test_invalid_parameter() { let credential = WinCredential::new_with_target(Some(""), "service", "user"); assert!( matches!(credential, Err(ErrorCode::Invalid(_, _))), "Created entry with empty target" ); } #[test] fn test_empty_service_and_user() { crate::tests::test_empty_service_and_user(entry_new); } #[test] fn test_missing_entry() { crate::tests::test_missing_entry(entry_new); } #[test] fn test_empty_password() { crate::tests::test_empty_password(entry_new); } #[test] fn test_round_trip_ascii_password() { crate::tests::test_round_trip_ascii_password(entry_new); } #[test] fn test_round_trip_non_ascii_password() { crate::tests::test_round_trip_non_ascii_password(entry_new); } #[test] fn test_round_trip_random_secret() { crate::tests::test_round_trip_random_secret(entry_new); } #[test] fn test_update() { crate::tests::test_update(entry_new); } #[test] fn test_get_update_attributes() { let name = generate_random_string(); let cred = WinCredential::new_with_target(None, &name, &name) .expect("Can't create credential for attribute test"); let entry = Entry::new_with_credential(Box::new(cred.clone())); assert!( matches!(entry.get_attributes(), Err(ErrorCode::NoEntry)), "Read missing credential in attribute test", ); let mut in_map: HashMap<&str, &str> = HashMap::new(); in_map.insert("label", "ignored label value"); in_map.insert("attribute name", "ignored attribute value"); in_map.insert("target_alias", "target alias value"); in_map.insert("comment", "comment value"); in_map.insert("username", "username value"); assert!( matches!(entry.update_attributes(&in_map), Err(ErrorCode::NoEntry)), "Updated missing credential in attribute test", ); // create the credential and test again entry .set_password("test password for attributes") .unwrap_or_else(|err| panic!("Can't set password for attribute test: {err:?}")); let out_map = entry .get_attributes() .expect("Can't get attributes after create"); assert_eq!(out_map["target_alias"], cred.target_alias); assert_eq!(out_map["comment"], cred.comment); assert_eq!(out_map["username"], cred.username); assert!( matches!(entry.update_attributes(&in_map), Ok(())), "Couldn't update attributes in attribute test", ); let after_map = entry .get_attributes() .expect("Can't get attributes after update"); assert_eq!(after_map["target_alias"], in_map["target_alias"]); assert_eq!(after_map["comment"], in_map["comment"]); assert_eq!(after_map["username"], in_map["username"]); assert!(!after_map.contains_key("label")); assert!(!after_map.contains_key("attribute name")); entry .delete_credential() .unwrap_or_else(|err| panic!("Can't delete credential for attribute test: {err:?}")); assert!( matches!(entry.get_attributes(), Err(ErrorCode::NoEntry)), "Read deleted credential in attribute test", ); } #[test] fn test_get_credential() { let name = generate_random_string(); let entry = entry_new(&name, &name); let password = "test get password"; entry .set_password(password) .expect("Can't set test get password"); let credential: &WinCredential = entry .get_credential() .downcast_ref() .expect("Not a windows credential"); let actual = credential.get_credential().expect("Can't read credential"); assert_eq!( actual.username, credential.username, "Usernames don't match" ); assert_eq!( actual.target_name, credential.target_name, "Target names don't match" ); assert_eq!( actual.target_alias, credential.target_alias, "Target aliases don't match" ); assert_eq!(actual.comment, credential.comment, "Comments don't match"); entry .delete_credential() .expect("Couldn't delete get-credential"); assert!(matches!(entry.get_password(), Err(ErrorCode::NoEntry))); } } keyring-3.6.3/tests/basic.rs000064400000000000000000000105251046102023000141010ustar 00000000000000use common::{generate_random_bytes_of_len, generate_random_string, init_logger}; use keyring::{Entry, Error}; mod common; #[test] fn test_missing_entry() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Missing entry has password" ) } #[test] #[cfg(all(target_os = "linux", not(feature = "linux-native")))] fn test_empty_password() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); let in_pass = ""; entry .set_password(in_pass) .expect("Can't set empty password"); let out_pass = entry.get_password().expect("Can't get empty password"); assert_eq!( in_pass, out_pass, "Retrieved and set empty passwords don't match" ); entry.delete_credential().expect("Can't delete password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted password" ) } #[test] fn test_round_trip_ascii_password() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); let password = "test ascii password"; entry .set_password(password) .expect("Can't set ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, password, "Retrieved and set ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted ascii password" ) } #[test] fn test_round_trip_non_ascii_password() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); let password = "このきれいな花は桜です"; entry .set_password(password) .expect("Can't set non-ascii password"); let stored_password = entry.get_password().expect("Can't get non-ascii password"); assert_eq!( stored_password, password, "Retrieved and set non-ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete non-ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted non-ascii password" ) } #[test] fn test_round_trip_random_secret() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); let secret = generate_random_bytes_of_len(24); entry .set_secret(secret.as_slice()) .expect("Can't set random secret"); let stored_secret = entry.get_secret().expect("Can't get random secret"); assert_eq!( &stored_secret, secret.as_slice(), "Retrieved and set random secrets don't match" ); entry .delete_credential() .expect("Can't delete random secret"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted random secret" ) } #[test] fn test_update() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); let password = "test ascii password"; entry .set_password(password) .expect("Can't set initial ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, password, "Retrieved and set initial ascii passwords don't match" ); let password = "このきれいな花は桜です"; entry .set_password(password) .expect("Can't update ascii with non-ascii password"); let stored_password = entry.get_password().expect("Can't get non-ascii password"); assert_eq!( stored_password, password, "Retrieved and updated non-ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete updated password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted updated password" ) } keyring-3.6.3/tests/common/mod.rs000064400000000000000000000025031046102023000150640ustar 00000000000000#![allow(dead_code)] // not all of these utilities are used by all tests /// When tests fail, they leave keys behind, and those keys /// have to be cleaned up before the tests can be run again /// in order to avoid bad results. So it's a lot easier just /// to have tests use a random string for key names to avoid /// the conflicts, and then do any needed cleanup once everything /// is working correctly. So tests make keys with these functions. /// When tests fail, they leave keys behind, and those keys /// have to be cleaned up before the tests can be run again /// in order to avoid bad results. So it's a lot easier just /// to have tests use a random string for key names to avoid /// the conflicts, and then do any needed cleanup once everything /// is working correctly. So we export this function for tests to use. pub fn generate_random_string_of_len(len: usize) -> String { use fastrand; use std::iter::repeat_with; repeat_with(fastrand::alphanumeric).take(len).collect() } pub fn generate_random_string() -> String { generate_random_string_of_len(30) } pub fn generate_random_bytes_of_len(len: usize) -> Vec { use fastrand; use std::iter::repeat_with; repeat_with(|| fastrand::u8(..)).take(len).collect() } pub fn init_logger() { let _ = env_logger::builder().is_test(true).try_init(); } keyring-3.6.3/tests/threading.rs000064400000000000000000000166701046102023000147740ustar 00000000000000use common::{generate_random_string, init_logger}; use keyring::{Entry, Error}; mod common; #[test] fn test_create_then_move() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).unwrap(); let test = move || { let password = "test ascii password"; entry .set_password(password) .expect("Can't set initial ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, password, "Retrieved and set initial ascii passwords don't match" ); let password = "このきれいな花は桜です"; entry .set_password(password) .expect("Can't set non-ascii password"); let stored_password = entry.get_password().expect("Can't get non-ascii password"); assert_eq!( stored_password, password, "Retrieved and set non-ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete non-ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted non-ascii password" ); }; let handle = std::thread::spawn(test); assert!(handle.join().is_ok(), "Couldn't execute on thread") } #[test] fn test_simultaneous_create_then_move() { init_logger(); let mut handles = vec![]; for i in 0..10 { let name = format!("{}-{}", generate_random_string(), i); let entry = Entry::new(&name, &name).expect("Can't create entry"); let test = move || { entry.set_password(&name).expect("Can't set ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, name, "Retrieved and set ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted ascii password" ); }; handles.push(std::thread::spawn(test)) } for handle in handles { handle.join().expect("Couldn't execute on thread") } } #[test] #[cfg(not(target_os = "windows"))] fn test_create_set_then_move() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); let password = "test ascii password"; entry .set_password(password) .expect("Can't set ascii password"); let test = move || { let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, password, "Retrieved and set ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted ascii password" ); }; let handle = std::thread::spawn(test); assert!(handle.join().is_ok(), "Couldn't execute on thread") } #[test] #[cfg(not(target_os = "windows"))] fn test_simultaneous_create_set_then_move() { init_logger(); let mut handles = vec![]; for i in 0..10 { let name = format!("{}-{}", generate_random_string(), i); let entry = Entry::new(&name, &name).expect("Can't create entry"); entry.set_password(&name).expect("Can't set ascii password"); let test = move || { let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, name, "Retrieved and set ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted ascii password" ); }; handles.push(std::thread::spawn(test)) } for handle in handles { handle.join().expect("Couldn't execute on thread") } } #[test] fn test_simultaneous_independent_create_set() { init_logger(); let mut handles = vec![]; for i in 0..10 { let name = format!("thread_entry{i}"); let test = move || { let entry = Entry::new(&name, &name).expect("Can't create entry"); entry.set_password(&name).expect("Can't set ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, name, "Retrieved and set ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted ascii password" ); }; handles.push(std::thread::spawn(test)) } for handle in handles { handle.join().expect("Couldn't execute on thread") } } #[test] #[cfg(any(target_os = "macos", target_os = "windows", feature = "linux-native"))] fn test_multiple_create_delete_single_thread() { init_logger(); let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); let repeats = 10; for _i in 0..repeats { entry.set_password(&name).expect("Can't set ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, name, "Retrieved and set ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted ascii password" ); } } #[test] #[cfg(any(target_os = "macos", target_os = "windows", feature = "linux-native"))] fn test_simultaneous_multiple_create_delete_single_thread() { init_logger(); let mut handles = vec![]; for t in 0..10 { let name = generate_random_string(); let test = move || { let name = format!("{name}-{t}"); let entry = Entry::new(&name, &name).expect("Can't create entry"); let repeats = 10; for _i in 0..repeats { entry.set_password(&name).expect("Can't set ascii password"); let stored_password = entry.get_password().expect("Can't get ascii password"); assert_eq!( stored_password, name, "Retrieved and set ascii passwords don't match" ); entry .delete_credential() .expect("Can't delete ascii password"); assert!( matches!(entry.get_password(), Err(Error::NoEntry)), "Able to read a deleted ascii password" ); } }; handles.push(std::thread::spawn(test)) } for handle in handles { handle.join().expect("Couldn't execute on thread") } }