keyring-3.2.1/.cargo_vcs_info.json0000644000000001360000000000100125070ustar { "git": { "sha1": "55789e3c5e0f02cc86980582538c911a139999fe" }, "path_in_vcs": "" }keyring-3.2.1/.gitignore000064400000000000000000000006011046102023000132640ustar 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.2.1/CHANGELOG.md000064400000000000000000000113031046102023000131060ustar 00000000000000## 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.2.1/Cargo.lock0000644000001346200000000000100104700ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[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 = "anstream" version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "async-broadcast" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "slab", ] [[package]] name = "async-fs" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock", "blocking", "futures-lite", ] [[package]] name = "async-io" version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "tracing", "windows-sys 0.59.0", ] [[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.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix", "tracing", "windows-sys 0.59.0", ] [[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.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", "windows-sys 0.59.0", ] [[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.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" 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.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ "async-channel", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[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.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[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.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_derive" version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[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-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1caa0c241c01ad8d99a78d553567d38f873dd3ac16eca33a5370d650ab25584e" 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.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "event-listener" version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[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.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 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.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[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.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[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.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "inout" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ "block-padding", "generic-array", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "js-sys" version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] [[package]] name = "keyring" version = "3.2.1" dependencies = [ "base64", "byteorder", "clap", "dbus-secret-service", "doc-comment", "linux-keyutils", "openssl", "rand", "rpassword", "secret-service", "security-framework", "whoami", "windows-sys 0.59.0", "zbus", ] [[package]] name = "libc" version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[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 2.6.0", "libc", ] [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[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.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", ] [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.6.0", "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.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.6.0", "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.3.1+3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" dependencies = [ "cc", ] [[package]] name = "openssl-sys" version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" 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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[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.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", "windows-sys 0.59.0", ] [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[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", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "rpassword" version = "7.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" dependencies = [ "libc", "rtoolbox", "windows-sys 0.48.0", ] [[package]] name = "rtoolbox" version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" dependencies = [ "libc", "windows-sys 0.48.0", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" version = "0.38.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[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 2.6.0", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_repr" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" 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.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "socket2" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.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.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix", "windows-sys 0.48.0", ] [[package]] name = "tokio" version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", "socket2", "tracing", "windows-sys 0.52.0", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[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.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[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.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[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.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "whoami" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" 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.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[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-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[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", "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_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[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_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[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_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[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_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[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_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] [[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.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] [[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.2.1/Cargo.toml0000644000000074240000000000100105140ustar # 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.2.1" authors = [ "Walther Chen ", "Daniel Brotsky ", ] build = false exclude = [".github/"] 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] features = [ "apple-native", "windows-native", "sync-secret-service", "crypto-rust", ] [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.openssl] version = "0.10.55" 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.rand] version = "0.8" [dev-dependencies.rpassword] version = "7" [dev-dependencies.whoami] version = "1" [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"] sync-secret-service = ["dep:dbus-secret-service"] tokio = ["zbus?/tokio"] vendored = [ "dbus-secret-service?/vendored", "openssl?/vendored", ] windows-native = [ "dep:windows-sys", "dep:byteorder", ] [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.security-framework] version = "2" optional = true [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 = "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 = "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.59" features = [ "Win32_Foundation", "Win32_Security_Credentials", ] optional = true keyring-3.2.1/Cargo.toml.orig000064400000000000000000000050471046102023000141740ustar 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.2.1" 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"] 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] openssl = { version = "0.10.55", optional = true } [target.'cfg(any(target_os = "macos", 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.59", features = ["Win32_Foundation", "Win32_Security_Credentials"], optional = true } [[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"] } rpassword = "7" rand = "0.8" doc-comment = "0.3" whoami = "1" [package.metadata.docs.rs] features = ["apple-native", "windows-native", "sync-secret-service", "crypto-rust"] keyring-3.2.1/LICENSE-APACHE000064400000000000000000000251321046102023000132260ustar 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.2.1/LICENSE-MIT000064400000000000000000000020461046102023000127350ustar 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.2.1/README.md000064400000000000000000000245311046102023000125630ustar 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://github.com/hwchen/keyring-rs) [![crates.io](https://img.shields.io/crates/v/keyring.svg?style=flat-square)](https://crates.io/crates/keyring) [![docs.rs](https://docs.rs/keyring/badge.svg)](https://docs.rs/keyring) A cross-platform library to manage storage and retrieval of passwords (and other secrets) in the underlying platform secure store, 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 and the kernel keyutils. * _FreeBSD_, _OpenBSD_: The DBus-based Secret Service. * _macOS_, _iOS_: The local keychain. * _Windows_: The Windows Credential Manager. To enable the stores you want, you use features: 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. (You cannot enable more than one keystore for a given platform, except when producing docs.) If you don't enable any credential stores that are supported on a specific target, the _mock_ keystore will be the default on that target. If you enable multiple credential stores for a specific target, you will get a compile error. See the [developer docs](https://docs.rs/keyring/) for details of which features control the inclusion of which credential stores (and which platforms each credential store targets). ### 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 - @stankec - @steveatinfincia - @Sytten - @thewh1teagle - @tmpfs - @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.2.1/build-xplat-docs.sh000064400000000000000000000004061046102023000150060ustar 00000000000000#!/bin/bash cargo doc --no-deps --target aarch64-unknown-linux-musl $OPEN_DOCS cargo doc --no-deps --target aarch64-pc-windows-msvc $OPEN_DOCS cargo doc --no-deps --target aarch64-apple-darwin $OPEN_DOCS cargo doc --no-deps --target aarch64-apple-ios $OPEN_DOCS keyring-3.2.1/examples/cli.rs000064400000000000000000000165001046102023000142340ustar 00000000000000extern crate keyring; use clap::Parser; 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 (secret, password) = args.get_password(); if let Some(secret) = secret { match entry.set_secret(&secret) { Ok(()) => args.success_message_for(Some(&secret), None), Err(err) => args.error_message_for(err), } } else if let Some(password) = password { match entry.set_password(&password) { Ok(()) => args.success_message_for(None, Some(&password)), Err(err) => args.error_message_for(err), } } else { if args.verbose { eprintln!("You must provide a password to the set command"); } std::process::exit(1) } } Command::Password => match entry.get_password() { Ok(password) => { println!("{password}"); args.success_message_for(None, Some(&password)); } Err(err) => args.error_message_for(err), }, Command::Secret => match entry.get_secret() { Ok(secret) => { println!("{}", secret_string(&secret)); args.success_message_for(Some(&secret), None); } Err(err) => args.error_message_for(err), }, Command::Delete => match entry.delete_credential() { Ok(()) => args.success_message_for(None, 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 in the secure store Set { #[clap(value_parser)] /// The password to set into the secure store. /// If it's a valid base64 encoding (with padding), /// it will be decoded and used to set the binary secret. /// Otherwise, it will be interpreted as a string password. /// If no password is specified, it will be /// collected interactively (without echo) /// from the terminal. password: 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, /// Delete the underlying credential from the secure store. Delete, } 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::Delete => { eprintln!("Couldn't delete credential for '{description}': {err}"); } }, } } std::process::exit(1) } fn success_message_for(&self, secret: Option<&[u8]>, password: Option<&str>) { if !self.verbose { return; } let description = self.description(); match self.command { Command::Set { .. } => { if let Some(pw) = password { eprintln!("Set password for '{description}' to '{pw}'"); } if let Some(secret) = secret { let secret = secret_string(secret); eprintln!("Set secret for '{description}' to decode of '{secret}'"); } } Command::Password => { let pw = password.unwrap(); eprintln!("Password for '{description}' is '{pw}'"); } Command::Secret => { let secret = secret_string(secret.unwrap()); eprintln!("Secret for '{description}' encodes as {secret}"); } Command::Delete => { eprintln!("Successfully deleted credential for '{description}'"); } } } fn get_password(&self) -> (Option>, Option) { match &self.command { Command::Set { password: Some(pw) } => password_or_secret(pw), Command::Set { password: None } => { if let Ok(password) = rpassword::prompt_password("Password: ") { password_or_secret(&password) } else { (None, None) } } _ => (None, None), } } } fn secret_string(secret: &[u8]) -> String { use base64::prelude::*; BASE64_STANDARD.encode(secret) } fn password_or_secret(input: &str) -> (Option>, Option) { use base64::prelude::*; match BASE64_STANDARD.decode(input) { Ok(secret) => (Some(secret), None), Err(_) => (None, Some(input.to_string())), } } keyring-3.2.1/examples/ios.rs000064400000000000000000000100501046102023000142510ustar 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.2.1/release.toml000064400000000000000000000000321046102023000136070ustar 00000000000000tag-name = 'v{{version}}' keyring-3.2.1/src/credential.rs000064400000000000000000000127351046102023000145560ustar 00000000000000/*! # Platorm-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 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<()>; /// 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 a password (a string) from the credential, if one has been set. /// /// This has no effect on the underlying store. fn get_password(&self) -> Result; /// Retrieve a secret (a byte array) from the credential, if one has been set. /// /// This has no effect on the underlying store. fn get_secret(&self) -> Result>; /// 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.2.1/src/error.rs000064400000000000000000000112571046102023000135730ustar 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.2.1/src/ios.rs000064400000000000000000000156251046102023000132370ustar 00000000000000/*! # iOS Keychain credential store iOS credential stores are called Keychains. On iOS there is only one of these. Generic credentials on iOS can be identified by a large number of _key/value_ attributes; this module (currently) uses only the _account_ and _name_ attributes. For a given service/user pair, this module targets a generic credential in the User (login) keychain whose _account_ is the user and and whose _name_ is 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.) On iOS, the target parameter is ignored, because there is only one keychain that can be targeted to store a generic credential. */ 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.2.1/src/keyutils.rs000064400000000000000000000357541046102023000143230ustar 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). # 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_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.2.1/src/lib.rs000064400000000000000000000530741046102023000132130ustar 00000000000000/*! # 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. Specifying multiple credential store features for a given platform is not supported, and will cause compile-time errors. There are no default features in this crate: you must specify explicitly which platform-specific credential stores you intend to use. Here are the available credential store features: - `apple-native`: Provides access to the Keychain credential store on macOS and iOS. - `windows-native`: Provides access to the Windows Credential Store on Windows. - `linux-native`: Provides access to the `keyutils` storage on Linux. - `sync-secret-service`: Provides access to the DBus-based [Secret Service](https://specifications.freedesktop.org/secret-service/latest/) storage on Linux, FreeBSD, and OpenBSD. This is a _synchronous_ keystore that provides support for encrypting secrets when they are transferred across the bus. If you wish to use this encryption support, additionally specify one (and only one) of the `crypto-rust` or `crypto-openssl` features (to choose the implementation libraries used for the encryption). By default, this keystore requires that the DBus library be installed on the user's machine (and the openSSL library if you specify it for encryption), but you can avoid this requirement by specifying the `vendored` feature (which will cause the build to include those libraries statically). - `async-secret-service`: Provides access to the DBus-based [Secret Service](https://specifications.freedesktop.org/secret-service/latest/) storage on Linux, FreeBSD, and OpenBSD. This is an _asynchronous_ keystore that always encrypts secrets when they are transferred across the bus. You _must_ specify both an async runtime feature (either `tokio` or `async-io`) and a cryptographic implementation (either `crypto-rust` or `crypto-openssl`) when using this keystore. If you want to use openSSL encryption but those libraries are not installed on the user's machine, specify the `vendored` feature to statically link them with the built crate. ## Client-provided Credential Stores In addition to the platform stores implemented by this crate, clients 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_password) rather than [get_password](Entry::get_secret). While this crate's code is thread-safe, the underlying credential stores may not handle access from different threads reliably. In particular, accessing the same credential from multiple threads at the same time can fail, especially on Windows and Linux, because the accesses may not be serialized in the same order they are made. And for RPC-based credential stores such as the dbus-based Secret Service, accesses from multiple threads (and even the same thread very quickly) are not recommended, as they may cause the RPC mechanism to fail. */ pub use credential::{Credential, CredentialBuilder}; pub use error::{Error, Result}; pub mod mock; // // no duplicate keystores on any platform // #[cfg(all( not(doc), any( all(feature = "linux-native", feature = "sync-secret-service"), all(feature = "linux-native", feature = "async-secret-service"), all(feature = "sync-secret-service", feature = "async-secret-service") ) ))] compile_error!("You can enable at most one keystore per target architecture"); // // Pick the *nix keystore // #[cfg(all(target_os = "linux", feature = "linux-native"))] pub mod keyutils; #[cfg(all( target_os = "linux", feature = "linux-native", not(all( doc, any(feature = "sync-secret-service", 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") ))] 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") ))] pub use secret_service as default; #[cfg(all( target_os = "linux", not(any( feature = "linux-native", feature = "sync-secret-service", feature = "async-secret-service" )) ))] pub use mock as default; #[cfg(all( any(target_os = "freebsd", target_os = "openbsd"), not(any(feature = "sync-secret-service", feature = "async-secret-service")) ))] pub use mock as default; // // pick the Apple keystore // #[cfg(all(target_os = "macos", feature = "apple-native"))] 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"))] 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"))] 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. pub fn new(service: &str, user: &str) -> Result { build_default_credential(None, service, user) } /// 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 { build_default_credential(Some(target), service, user) } /// Create an entry that uses the given platform credential for storage. pub fn new_with_credential(credential: Box) -> Entry { 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<()> { 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<()> { 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 { 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> { self.inner.get_secret() } /// 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<()> { 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 rand::Rng; /// 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:?}") } } } /// 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 { // from the Rust Cookbook: // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html use rand::{distributions::Alphanumeric, thread_rng, Rng}; thread_rng() .sample_iter(&Alphanumeric) .take(len) .map(char::from) .collect() } pub fn generate_random_string() -> String { generate_random_string_of_len(30) } 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 mut secret: [u8; 16] = [0; 16]; rand::rngs::OsRng.fill(&mut secret); test_round_trip_secret("non-ascii password", &entry, &secret); } 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, "このきれいな花は桜です", ); } } keyring-3.2.1/src/macos.rs000064400000000000000000000313201046102023000135350ustar 00000000000000/*! # macOS Keychain credential store macOS credential stores are called keychains. The OS automatically creates three of them (or four if removable media is being used). Generic credentials on macOS can be identified by a large number of _key/value_ attributes; this module (currently) uses only the _account_ and _name_ attributes. For a given service/user pair, this module targets a generic credential in the User (login) keychain whose _account_ is the user and whose _name_ is 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, generic credentials created by this module show up in the passwords area (with their _where_ field equal to their _name_). _Note_ entries on Mac are also generic credentials and notes created by third-party applications can be accessed by this module if you know their _account_ value (not displayed by _Keychain Access_). But because the difference between a password and a note is platform-dependent, there's no way to _create_ a note in this module. You can specify targeting a different keychain by passing the keychain's (case-insensitive) name as the target parameter to `Entry::new_with_target`. Any name other than one of the OS-supplied keychains (User, Common, System, and Dynamic) will be mapped to `User`. */ 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_vec()) } /// 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_vec()) } /// 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))); } } keyring-3.2.1/src/mock.rs000064400000000000000000000253411046102023000133720ustar 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. 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 [NotFound](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).unwrap(); 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_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.2.1/src/secret_service.rs000064400000000000000000000721431046102023000154500ustar 00000000000000/*! # secret-service credential store Items in the secret-service are identified by an arbitrary collection of attributes, and each has "label" for use in graphical editors. This implementation uses 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) - `application` (optional & always set to `rust-keyring`) 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 always created with all three search attributes, and they are given a label that identifies the crate and version and attributes used in the entry. If a target other than `default` is specified for the entry, then a collection labeled with that target will be created (if necessary) to hold the new item. 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 { let passwords: Vec = self.map_matching_items(get_item_password, true)?; Ok(passwords[0].clone()) } /// 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> { let secrets: Vec> = self.map_matching_items(get_item_secret, true)?; Ok(secrets[0].clone()) } /// 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!( "keyring-rs v{} for target '{target}', service '{service}', user '{user}'", 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 { let credentials = self.map_matching_items(Self::new_from_item, true)?; Ok(credentials[0].clone()) } /// 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 && matches!(self.target.as_ref(), Some(t) if t == "default") { 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.len() == 0 && 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 and decode its password. pub fn get_item_secret(item: &Item) -> Result> { let secret = item.get_secret().map_err(decode_error)?; Ok(secret) } // 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 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] #[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.2.1/src/windows.rs000064400000000000000000000622061046102023000141340ustar 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.) ## 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 byteorder::{ByteOrder, LittleEndian}; 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 super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi}; use super::error::{Error as ErrorCode, Result}; /// 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 blob_u16 = to_wstr_no_null(password); let mut blob = vec![0; blob_u16.len() * 2]; LittleEndian::write_u16_into(&blob_u16, &mut blob); self.set_secret(&blob) } /// 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)?; 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); // Password strings are converted to UTF-16, because that's the native // charset for Windows strings. This allows editing of the password in // the Windows native UI. But the storage for the credential is actually // a little-endian blob, because passwords can contain anything. 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 match unsafe { CredWriteW(p_credential, 0) } { 0 => 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 { 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) } /// 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(()) } /// 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 free the allocated 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 metadata = format!("keyring-rs v{VERSION} for service '{service}', user '{user}'"); 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: metadata, } } 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: metadata, } }; 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 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)); } // Now we know this _can_ be a UTF-16 string, so convert it to // as 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); String::from_utf16(&blob_u16).map_err(|_| ErrorCode::BadEncoding(blob)) } 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 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_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.2.1/tests/basic.rs000064400000000000000000000102661046102023000140750ustar 00000000000000use common::generate_random_string; use keyring::{Entry, Error}; mod common; #[test] fn test_missing_entry() { 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() { 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() { 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() { 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() { use rand::{rngs::OsRng, Rng}; let name = generate_random_string(); let entry = Entry::new(&name, &name).expect("Can't create entry"); let mut secret: [u8; 16] = [0; 16]; OsRng.fill(&mut secret); entry.set_secret(&secret).expect("Can't set random secret"); let stored_secret = entry.get_secret().expect("Can't get random secret"); assert_eq!( &stored_secret, &secret, "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() { 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.2.1/tests/common/mod.rs000064400000000000000000000014621046102023000150610ustar 00000000000000/// 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. pub fn generate_random_string_of_len(len: usize) -> String { // from the Rust Cookbook: // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html use rand::{distributions::Alphanumeric, thread_rng, Rng}; thread_rng() .sample_iter(&Alphanumeric) .take(len) .map(char::from) .collect() } pub fn generate_random_string() -> String { generate_random_string_of_len(30) } keyring-3.2.1/tests/threading.rs000064400000000000000000000164351046102023000147650ustar 00000000000000use common::generate_random_string; use keyring::{Entry, Error}; mod common; #[test] fn test_create_then_move() { 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() { 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() { 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() { 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() { 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() { 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() { 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") } }