rbw-1.13.2/.cargo_vcs_info.json0000644000000001360000000000100117120ustar { "git": { "sha1": "90e895f73fc0823644a90ef400249df1b5aa83c9" }, "path_in_vcs": "" }rbw-1.13.2/CHANGELOG.md000064400000000000000000000333721046102023000123230ustar 00000000000000# Changelog ## [1.13.2] - 2025-01-06 ## Fixed * Try another clipboard backend to try to fix cross platform issues. (Mag Mell, #226) * `rbw unlocked` no longer starts the agent if it isn't running. (#223) * The cardholder_name field is now correctly populated for card entries. (#204) * Fix ip address url matching when using the domain match type. (#211) * Make the behavior of matching urls with no paths when using the exact match type more consistent. (#211) ## [1.13.1] - 2024-12-27 ### Fixed * Moved clipboard support to a (default-enabled) feature, since not all platforms support it (disabling this feature should allow Android builds to work again). ## [1.13.0] - 2024-12-26 ### Fixed * Fix preventing the password type selectors in `rbw generate` from being used together. (antecrescent, #198) * Fix `--clipboard` on Wayland (Maksim Karelov, #192) * Fix parsing vaults with entries that have non-null field types (Tin Lai, #212) * Fix lock timeout being reset when checking version (aeber, #216) * Update API request headers to pass new stricter validation on the official bitwarden.com server (Davide Laezza, #219) * Make it possible to start the rbw agent process from a graphical session and then access it over SSH (Wim de With, #221) ## [1.12.1] - 2024-07-28 ### Fixed * Fix decrypting folder names of entries with individual item encryption keys. ## [1.12.0] - 2024-07-28 *NOTE: If you were affected by issue #163 (getting messages like `failed to decrypt encrypted secret: invalid mac` when doing any operations on your vault), you will need to `rbw sync` after upgrading in order to update your local vault with the necessary new data.* ### Fixed * Support decrypting entries encrypted with invididual item encryption keys, which are now generated by default from the official Bitwarden clients. (#163) * Correctly handle lowercased and padded base32 TOTP secrets. (owl, #189) * Make locking agent memory to RAM optional, since it appears to not always be available. (#143) ## [1.11.1] - 2024-06-26 ### Fixed * Updated the prelogin API endpoint to use the identity API instead of the base API, to correspond with upcoming changes to the official Bitwarden server (see https://github.com/bitwarden/server/pull/4206) ## [1.11.0] - 2024-06-20 ### Added * Support SSO login. (dezeroku, #174) * Added `rbw search`, which finds and displays the name of entries matching a given search term. * Added `--ignorecase` as an option to several subcommands. (Maximilian Götsch, #164) * The JSON output given by `--raw` now also includes the field type. ### Fixed * Fixed the client id used when logging in, which was causing problems with the official Bitwarden server. (Merlin Marek, #186) * Reworked `rbw-pinentry-keyring` to support passwords with spaces and 2fa codes. (Henk van Maanen, #178) * Try less hard to parse input as a url (so that using `rbw get` on an entry name containing a `:` works as expected). ## [1.10.2] - 2024-05-20 ### Fixed * Fix logging into the official Bitwarden server due to changes on their end (Gabriel Górski, #175) ## [1.10.1] - 2024-05-08 ### Added * `rbw code` supports TOTP codes which use a SHA256 or SHA512 hash (Jonas, #172) ### Fixed * Fix `rbw code` searching by UUID (Robert Günzler, #169) ## [1.10.0] - 2024-04-20 ### Added * `rbw get` now supports searching by URL as well (proxict, #132) * `rbw code` now supports `--clipboard`, and has an alias of `rbw totp` (#127) ### Changed * Set a user agent for all API calls, not just logging in (#165) ### Fixed * Also create runtime directories when running with `--no-daemonize` (Wim de With, #155) * Fix builds on NetBSD (#105) * Fix logging in when the configured email address differs in case from the email address used when registering (#158) * Fix editing passwords inadvertently clearing custom field values (#142) ## [1.9.0] - 2024-01-01 ### Added * Secure notes can now be edited (Tin Lai, #137) * Piping passwords to `rbw edit` is now possible (Tin Lai, #138) ### Fixed * More consistent behavior from `rbw get --field`, and fix some panics (Jörg Thalheim, #131) * Fix handling of pinentry EOF (Jörg Thalheim, #140) * Pass a user agent header to fix logging into the official bitwarden server (Maksim Karelov, #151) * Support the official bitwarden.eu server (Edvin Åkerfeldt, #152) ## [1.8.3] - 2023-07-20 ### Fixed * Fixed running on linux without an X11 context available. (Benjamin Jacobs, #126) ## [1.8.2] - 2023-07-19 ### Fixed * Fixed several issues with notification-based background syncing, it should be much more reliable now. ## [1.8.1] - 2023-07-18 ### Fixed * `rbw config set notifications_url` now actually works ## [1.8.0] - 2023-07-18 ### Added * `rbw get --clipboard` to copy the result to the clipboard instead of displaying it on stdout. (eatradish, #120) * Background syncing now additionally happens when the server notifies the agent of password updates, instead of needing to wait for the `sync_interval` timer. (Bernd Schoolman, #115) * New helper script `rbw-pinentry-keyring` which can be used as an alternate pinentry program (via `rbw config set pinentry rbw-pinentry-keyring`) to automatically read the master password from the system keyring. Currently only supports the Gnome keyring via `secret-tool`. (Kai Frische, #122) * Yubikeys in OTP mode are now supported for logging into a Bitwarden server. (troyready, #123) ### Fixed * Better error reporting when `rbw login` or `rbw register` fail. ## [1.7.1] - 2023-03-27 ### Fixed * argon2 actually works now (#113, Bernd Schoolmann) ## [1.7.0] - 2023-03-25 ### Added * `rbw` now automatically syncs the database from the server at a specified interval while it is running. This defaults to once an hour, but is configurable via the `sync_interval` option * Email 2FA is now supported (#111, René 'Necoro' Neumann) * argon2 KDF is now supported (#109, Bernd Schoolmann) ### Fixed * `rbw --version` now works again ## [1.6.0] - 2023-03-09 ### Added * `rbw get` now supports a `--raw` option to display the entire contents of the entry in JSON format (#97, classabbyamp) ## [1.5.0] - 2023-02-18 ### Added * Support for authenticating to self-hosted Bitwarden servers using client certificates (#92, Filipe Pina) * Support multiple independent profiles via the `RBW_PROFILE` environment variable (#93, Skia) * Add `rbw get --field` (#95, Jericho Keyne) ### Fixed * Don't panic when not all stdout is read (#82, witcher) * Fixed duplicated alias names in help output (#46) ## [1.4.3] - 2022-02-10 ### Fixed * Restored packaged scripts to the crate bundle, since they are used by some downstream packages (no functional changes) (#81) ## [1.4.2] - 2022-02-10 ### Changed * Device id is now stored in a separate file in the local data directory instead of as part of the config (#74) ### Fixed * Fix api renaming in official bitwarden server (#80) ## [1.4.1] - 2021-10-28 ### Added * `bin/git-credential-rbw` to be used as a [git credential helper](https://git-scm.com/docs/gitcredentials#_custom_helpers) (#41, xPMo) ### Changed * Also disable swap and viminfo files when using `EDITOR=nvim` (#70, Dophin2009) ### Fixed * Properly handle a couple folder name edge cases in `bin/rbw-fzf` (#66, mattalexx) * Support passing command line arguments via `EDITOR`/`VISUAL` (#61, xPMo) ## [1.4.0] - 2021-10-27 ### Fixed * Add `rbw register` to allow `rbw` to work with the official Bitwarden server again - see the README for details (#71) ## [1.3.0] - 2021-07-05 ### Changed * Use the system's native TLS certificate store when making HTTP requests. ### Fixed * Correctly handle TOTP secret strings that copy with spaces (#56, TamasBarta, niki-on-github) ## [1.2.0] - 2021-04-18 ### Added * Shell completion for bash, zsh, and fish (#18) ### Changed * Prebuilt binaries are now statically linked using musl, to prevent glibc version issues once and for all (#47) * Standardize on RustCrypto in preference to ring or openssl ### Fixed * `rbw generate` can now choose the same character more than once (#54, rjc) * Improved handling of password history for entries with no password (#51/#53, simias) * Fix configuring base_url with a trailing slash when using a self-hosted version of the official bitwarden server (#49, phylor) ## [1.1.2] - 2021-03-06 ### Fixed * Send warnings about failure to disable PTRACE_ATTACH to the agent logs rather than stderr ## [1.1.1] - 2021-03-05 ### Fixed * Fix non-Linux platforms (#44, rjc) ## [1.1.0] - 2021-03-02 ### Added * You can now `rbw config set pinentry pinentry-curses` to change the pinentry program used by `rbw` (#39, djmattyg007) ### Changed * On Linux, the `rbw-agent` process can no longer be attached to by debuggers, and no longer produces core dumps (#42, oranenj) * Suggest rotating the user's encryption key if we see an old cipherstring type (#40, rjc) * Prefer the value of `$VISUAL` when trying to find an editor to run, before falling back to `$EDITOR` (#43, rjc) ## [1.0.0] - 2021-02-21 ### Added * Clarified the maintenance policy for this project in the README ### Fixed * Stop hardcoding /tmp when using the fallback runtime directory (#37, pschmitt) * Fix `rbw edit` clearing the match detection setting for websites associated with the edited password (#34, AdmiralNemo) * Note that you will need to `rbw sync` after upgrading and before running `rbw edit` in order to correctly update the local database. ## [0.5.2] - 2020-12-02 ### Fixed * `rbw` should once again be usable on systems with glibc-2.28 (such as Debian stable). ## [0.5.1] - 2020-12-02 ### Fixed * `rbw code` now always displays the correct number of digits. (#25, Tyilo) * TOTP secrets can now also be supplied as `otpauth` urls. * Logging into bitwarden.com with 2fa enabled now works again. ## [0.5.0] - 2020-10-12 ### Added * Add support for cipherstring type 6 (fixes some vaults using an older format for organizations data). (Jake Swenson) * `rbw get --full` now displays URIs, TOTP secrets, and custom fields. * Add `rbw code` for generating TOTP codes based on secrets stored in Bitwarden. * Add `rbw unlocked` which will exit with success if the agent is unlocked and failure if the agent is locked. ### Fixed * Don't display deleted items (#22, GnunuX) ## [0.4.6] - 2020-07-11 ### Fixed * Login passwords containing a `%` now work properly (albakham). ## [0.4.5] - 2020-07-11 ### Fixed * The pinentry window now no longer times out. ## [0.4.4] - 2020-06-23 ### Fixed * Fix regression in `rbw get` when not specifying a folder. ## [0.4.3] - 2020-06-23 ### Added * `rbw get` now accepts a `--folder` option to pick the folder to search in. ### Changed * `rbw get --full` now also includes the username. (Jarkko Oranen) ### Fixed * `rbw` should now be usable on systems with glibc-2.28 (such as Debian stable). (incredible-machine) ## [0.4.2] - 2020-05-30 ### Fixed * `rbw` now no longer requires the `XDG_RUNTIME_DIR` environment variable to be set. ## [0.4.1] - 2020-05-28 ### Fixed * More improved error messages. ## [0.4.0] - 2020-05-28 ### Added * Authenticator-based two-step login is now supported. ### Fixed * Correctly handle password retries when entering an invalid password on the official Bitwarden server. * Fix hang when giving an empty string to pinentry. * The error message from the server is now shown when logging in fails. ## [0.3.5] - 2020-05-25 ### Fixed * Terminal-based pinentry methods should now work correctly (Glandos). * Further error message improvements. ## [0.3.4] - 2020-05-24 ### Fixed * Handle edge case where a URI entry is set for a cipher but that entry has a null URI string (Adrien CLERC). ## [0.3.3] - 2020-05-23 ### Fixed * Set the correct default lock timeout when first creating the config file. * Add a more useful error when `rbw` is run without being configured first. * Don't throw an error when attempting to configure the base url before configuring the email. * More improvements to error output. ## [0.3.2] - 2020-05-23 ### Fixed * Improve warning and error output a bit. ## [0.3.1] - 2020-05-23 ### Fixed * Fix option parsing for `rbw list --fields` and `rbw --uri` which was inadvertently broken in the previous release. ## [0.3.0] - 2020-05-22 ### Fixed * Better error message if the agent fails to start after daemonizing. * Always automatically upgrade rbw-agent on new releases. * Changing configuration now automatically drops in-memory keys (this should avoid errors when switching between different servers or accounts). * Disallow setting `lock_timeout` to `0`, since this will cause the agent to immediately drop the decrypted keys before they can be used for decryption, even within a single run of the `rbw` client. ## [0.2.2] - 2020-05-17 ### Fixed * Fix syncing from the official Bitwarden server (thanks the_fdw). ### Added * Added a couple example scripts to the repository for searching using fzf and rofi. Contributions and improvements welcome! ## [0.2.1] - 2020-05-03 ### Fixed * Properly maintain folder and URIs when editing an entry. ## [0.2.0] - 2020-05-03 ### Added * Multi-server support - you can now switch between multiple different bitwarden servers with `rbw config set base_url` without needing to redownload the password database each time. * `rbw config unset` to reset configuration items back to the default * `rbw list` and `rbw get` now support card, identity, and secure note entry types ### Fixed * `rbw` is now able to decrypt secrets from organizations you are a member of. * `rbw stop-agent` now waits for the agent to exit before returning. ### Changed * Move to the `ring` crate for a bunch of the cryptographic functionality. * The agent protocol is now versioned, to allow for seamless updates. ## [0.1.1] - 2020-05-01 ### Fixed * Some packaging changes. ## [0.1.0] - 2020-04-20 ### Added * Initial release rbw-1.13.2/Cargo.lock0000644000002315650000000000100077010ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", "windows-sys 0.59.0", ] [[package]] name = "anyhow" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arboard" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ "clipboard-win", "log", "objc2", "objc2-app-kit", "objc2-foundation", "parking_lot", "wl-clipboard-rs", "x11rb", ] [[package]] name = "argon2" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", "cpufeatures", "password-hash", ] [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-trait" version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", "bytes", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "rustversion", "serde", "serde_json", "serde_path_to_error", "serde_urlencoded", "sync_wrapper", "tokio", "tower", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-core" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", "futures-util", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "rustversion", "sync_wrapper", "tower-layer", "tower-service", "tracing", ] [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "base32" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[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 = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ "digest", ] [[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 = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ "objc2", ] [[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.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ "cipher", ] [[package]] name = "cc" version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" 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.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[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.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_complete" version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9" dependencies = [ "clap", ] [[package]] name = "clap_derive" version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" 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.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] [[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 = "daemonize" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" dependencies = [ "libc", ] [[package]] name = "data-encoding" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "der" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] [[package]] name = "derive-new" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", "crypto-common", "subtle", ] [[package]] name = "directories" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74be3be809c18e089de43bdc504652bb2bc473fca8756131f8689db8cf079ba9" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" dependencies = [ "libc", "redox_users", "windows-sys 0.45.0", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dlib" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ "libloading", ] [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", "env_filter", "humantime", "log", ] [[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.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "error-code" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "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 = "gethostname" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", "windows-targets 0.48.5", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[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.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[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 = "http" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http", "hyper", "hyper-util", "rustls", "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", ] [[package]] name = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 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 = "ipnet" version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-docker" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" dependencies = [ "once_cell", ] [[package]] name = "is-terminal" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ "hermit-abi", "libc", "windows-sys 0.52.0", ] [[package]] name = "is-wsl" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" dependencies = [ "is-docker", "once_cell", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.48.5", ] [[package]] name = "libm" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 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 = "litemap" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "mach2" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" dependencies = [ "libc", ] [[package]] name = "matchit" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi", "windows-sys 0.52.0", ] [[package]] name = "nix" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.1.1", "libc", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-bigint-dig" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ "byteorder", "lazy_static", "libm", "num-integer", "num-iter", "num-traits", "rand", "smallvec", "zeroize", ] [[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-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] [[package]] name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", "objc2-encode", ] [[package]] name = "objc2-app-kit" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.6.0", "block2", "libc", "objc2", "objc2-core-data", "objc2-core-image", "objc2-foundation", "objc2-quartz-core", ] [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", ] [[package]] name = "objc2-core-image" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", "objc2", "objc2-foundation", "objc2-metal", ] [[package]] name = "objc2-encode" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" [[package]] name = "objc2-foundation" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.6.0", "block2", "libc", "objc2", ] [[package]] name = "objc2-metal" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", ] [[package]] name = "objc2-quartz-core" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.6.0", "block2", "objc2", "objc2-foundation", "objc2-metal", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "open" version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" dependencies = [ "is-wsl", "libc", "pathdiff", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_pipe" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "password-hash" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core", "subtle", ] [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", "hmac", ] [[package]] name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", ] [[package]] name = "pin-project-lite" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der", "pkcs8", "spki", ] [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", ] [[package]] name = "pkg-config" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[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-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", ] [[package]] name = "quinn" version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ "bytes", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", "socket2", "thiserror 2.0.9", "tokio", "tracing", ] [[package]] name = "quinn-proto" version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", "getrandom", "rand", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", "thiserror 2.0.9", "tinyvec", "tracing", "web-time", ] [[package]] name = "quinn-udp" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", "socket2", "tracing", "windows-sys 0.59.0", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 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 = "rbw" version = "1.13.2" dependencies = [ "aes", "anyhow", "arboard", "argon2", "arrayvec", "axum", "base32", "base64", "block-padding", "cbc", "clap", "clap_complete", "daemonize", "directories", "env_logger", "futures", "futures-channel", "futures-util", "hkdf", "hmac", "humantime", "is-terminal", "libc", "log", "open", "pbkdf2", "percent-encoding", "pkcs8", "rand", "regex", "region", "reqwest", "rmpv", "rsa", "rustix", "serde", "serde_json", "serde_path_to_error", "serde_repr", "sha1", "sha2", "tempfile", "terminal_size", "textwrap", "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-tungstenite", "totp-lite", "url", "urlencoding", "uuid", "zeroize", ] [[package]] name = "redox_syscall" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", "thiserror 1.0.69", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "region" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" dependencies = [ "bitflags 1.3.2", "libc", "mach2", "windows-sys 0.52.0", ] [[package]] name = "reqwest" version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-util", "ipnet", "js-sys", "log", "mime", "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "windows-registry", ] [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rmp" version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", "num-traits", "paste", ] [[package]] name = "rmpv" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" dependencies = [ "num-traits", "rmp", ] [[package]] name = "rsa" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", "rand_core", "signature", "spki", "subtle", "zeroize", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustix" version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "itoa", "libc", "linux-raw-sys", "once_cell", "windows-sys 0.59.0", ] [[package]] name = "rustls" version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ "rustls-pki-types", ] [[package]] name = "rustls-pki-types" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" dependencies = [ "web-time", ] [[package]] name = "rustls-webpki" version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc" dependencies = [ "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ "itoa", "serde", ] [[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 = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[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 = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smawk" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spki" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[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.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] [[package]] name = "synstructure" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tempfile" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "terminal_size" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", "windows-sys 0.59.0", ] [[package]] name = "textwrap" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", "unicode-width", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ "thiserror-impl 2.0.9", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-rustls" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-stream" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-tungstenite" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", "rustls", "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", "tungstenite", ] [[package]] name = "totp-lite" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8e43134db17199f7f721803383ac5854edd0d3d523cc34dba321d6acfbe76c3" dependencies = [ "digest", "hmac", "sha1", "sha2", ] [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "tree_magic_mini" version = "3.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" dependencies = [ "fnv", "memchr", "nom", "once_cell", "petgraph", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" dependencies = [ "byteorder", "bytes", "data-encoding", "http", "httparse", "log", "rand", "rustls", "rustls-pki-types", "sha1", "thiserror 1.0.69", "url", "utf-8", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-linebreak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", ] [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wayland-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", "rustix", "scoped-tls", "smallvec", "wayland-sys", ] [[package]] name = "wayland-client" version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ "bitflags 2.6.0", "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", ] [[package]] name = "wayland-protocols-wlr" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-scanner", ] [[package]] name = "wayland-scanner" version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", "quick-xml", "quote", ] [[package]] name = "wayland-sys" version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", "pkg-config", ] [[package]] name = "web-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "web-time" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "windows-registry" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ "windows-result", "windows-strings", "windows-targets 0.52.6", ] [[package]] name = "windows-result" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ "windows-result", "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[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.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[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.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[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.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[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.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[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.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[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.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[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.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[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.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[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 = "wl-clipboard-rs" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" dependencies = [ "derive-new", "libc", "log", "nix", "os_pipe", "tempfile", "thiserror 1.0.69", "tree_magic_mini", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", ] [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "x11rb" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", "rustix", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "yoke" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[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 = "zerofrom" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", "syn", ] rbw-1.13.2/Cargo.toml0000644000000120630000000000100077120ustar # 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" name = "rbw" version = "1.13.2" authors = ["Jesse Luehrs "] build = false include = [ "src/**/*", "bin/**/*", "LICENSE", "README.md", "CHANGELOG.md", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Unofficial Bitwarden CLI" readme = "README.md" keywords = ["bitwarden"] categories = [ "command-line-utilities", "cryptography", ] license = "MIT" repository = "https://git.tozt.net/rbw" [package.metadata.deb] assets = [ [ "target/release/rbw", "usr/bin/", "755", ], [ "target/release/rbw-agent", "usr/bin/", "755", ], [ "target/release/completion/bash", "usr/share/bash-completion/completions/rbw", "644", ], [ "target/release/completion/zsh", "usr/share/zsh/vendor-completions/_rbw", "644", ], [ "target/release/completion/fish", "usr/share/fish/completions/rbw.fish", "644", ], [ "README.md", "usr/share/doc/rbw/README", "644", ], ] depends = "pinentry" license-file = ["LICENSE"] [lib] name = "rbw" path = "src/lib.rs" [[bin]] name = "rbw" path = "src/bin/rbw/main.rs" [[bin]] name = "rbw-agent" path = "src/bin/rbw-agent/main.rs" [dependencies.aes] version = "0.8.4" [dependencies.anyhow] version = "1.0.95" [dependencies.arboard] version = "3.4" features = ["wayland-data-control"] optional = true default-features = false [dependencies.argon2] version = "0.5.3" [dependencies.arrayvec] version = "0.7.6" [dependencies.axum] version = "0.7.9" [dependencies.base32] version = "0.5.1" [dependencies.base64] version = "0.22.1" [dependencies.block-padding] version = "0.3.3" [dependencies.cbc] version = "0.1.2" features = [ "alloc", "std", ] [dependencies.clap] version = "4.5.23" features = [ "wrap_help", "derive", ] [dependencies.clap_complete] version = "4.5.40" [dependencies.daemonize] version = "0.5.0" [dependencies.directories] version = "=5.0.0" [dependencies.env_logger] version = "0.11.6" [dependencies.futures] version = "0.3.31" [dependencies.futures-channel] version = "0.3.31" [dependencies.futures-util] version = "0.3.31" [dependencies.hkdf] version = "0.12.4" [dependencies.hmac] version = "0.12.1" features = ["std"] [dependencies.humantime] version = "2.1.0" [dependencies.is-terminal] version = "0.4.13" [dependencies.libc] version = "0.2.169" [dependencies.log] version = "0.4.22" [dependencies.open] version = "5.3.1" [dependencies.pbkdf2] version = "0.12.2" [dependencies.percent-encoding] version = "2.3.1" [dependencies.pkcs8] version = "0.10.2" [dependencies.rand] version = "0.8.5" [dependencies.regex] version = "1.11.1" [dependencies.region] version = "3.0.2" [dependencies.reqwest] version = "0.12.9" features = [ "blocking", "json", "rustls-tls-native-roots", ] default-features = false [dependencies.rmpv] version = "1.3.0" [dependencies.rsa] version = "0.9.7" [dependencies.rustix] version = "0.38.42" features = [ "termios", "procfs", "process", "pipe", ] [dependencies.serde] version = "1.0.216" features = ["derive"] [dependencies.serde_json] version = "1.0.134" [dependencies.serde_path_to_error] version = "0.1.16" [dependencies.serde_repr] version = "0.1.19" [dependencies.sha1] version = "0.10.6" [dependencies.sha2] version = "0.10.8" [dependencies.tempfile] version = "3.14.0" [dependencies.terminal_size] version = "0.4.1" [dependencies.textwrap] version = "0.16.1" [dependencies.thiserror] version = "1.0.63" [dependencies.tokio] version = "1.42.0" features = ["full"] [dependencies.tokio-stream] version = "0.1.17" features = ["net"] [dependencies.tokio-tungstenite] version = "0.24" features = [ "rustls-tls-native-roots", "url", ] [dependencies.totp-lite] version = "2.0.1" [dependencies.url] version = "2.5.4" [dependencies.urlencoding] version = "2.1.3" [dependencies.uuid] version = "1.11.0" features = ["v4"] [dependencies.zeroize] version = "1.8.1" [features] clipboard = ["arboard"] default = ["clipboard"] [lints.clippy] as_conversions = "warn" cognitive_complexity = "allow" get_unwrap = "warn" large_enum_variant = "allow" missing_const_for_fn = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" multiple_crate_versions = "allow" must_use_candidate = "allow" significant_drop_tightening = "allow" similar_names = "allow" struct_excessive_bools = "allow" too_many_arguments = "allow" too_many_lines = "allow" type_complexity = "allow" [lints.clippy.cargo] level = "warn" priority = -1 [lints.clippy.nursery] level = "warn" priority = -1 [lints.clippy.pedantic] level = "warn" priority = -1 rbw-1.13.2/Cargo.toml.orig000064400000000000000000000062771046102023000134050ustar 00000000000000[package] name = "rbw" version = "1.13.2" authors = ["Jesse Luehrs "] edition = "2021" description = "Unofficial Bitwarden CLI" repository = "https://git.tozt.net/rbw" readme = "README.md" keywords = ["bitwarden"] categories = ["command-line-utilities", "cryptography"] license = "MIT" include = ["src/**/*", "bin/**/*", "LICENSE", "README.md", "CHANGELOG.md"] [dependencies] aes = "0.8.4" anyhow = "1.0.95" argon2 = "0.5.3" arrayvec = "0.7.6" axum = "0.7.9" base32 = "0.5.1" base64 = "0.22.1" block-padding = "0.3.3" cbc = { version = "0.1.2", features = ["alloc", "std"] } clap_complete = "4.5.40" clap = { version = "4.5.23", features = ["wrap_help", "derive"] } daemonize = "0.5.0" # TODO: directories 5.0.1 uses MPL code, which isn't license-compatible # we should switch to something else at some point directories = "=5.0.0" env_logger = "0.11.6" futures = "0.3.31" futures-channel = "0.3.31" futures-util = "0.3.31" hkdf = "0.12.4" hmac = { version = "0.12.1", features = ["std"] } humantime = "2.1.0" is-terminal = "0.4.13" libc = "0.2.169" log = "0.4.22" open = "5.3.1" pbkdf2 = "0.12.2" percent-encoding = "2.3.1" pkcs8 = "0.10.2" rand = "0.8.5" regex = "1.11.1" region = "3.0.2" reqwest = { version = "0.12.9", default-features = false, features = ["blocking", "json", "rustls-tls-native-roots"] } rmpv = "1.3.0" rsa = "0.9.7" rustix = { version = "0.38.42", features = ["termios", "procfs", "process", "pipe"] } serde_json = "1.0.134" serde_path_to_error = "0.1.16" serde_repr = "0.1.19" serde = { version = "1.0.216", features = ["derive"] } sha1 = "0.10.6" sha2 = "0.10.8" tempfile = "3.14.0" terminal_size = "0.4.1" textwrap = "0.16.1" thiserror = "1.0.63" tokio-stream = { version = "0.1.17", features = ["net"] } tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots", "url"] } tokio = { version = "1.42.0", features = ["full"] } totp-lite = "2.0.1" url = "2.5.4" urlencoding = "2.1.3" uuid = { version = "1.11.0", features = ["v4"] } zeroize = "1.8.1" arboard = { version = "3.4", default-features = false, features = ["wayland-data-control"], optional = true } [features] default = ["clipboard"] clipboard = ["arboard"] [lints.clippy] cargo = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 } as_conversions = "warn" get_unwrap = "warn" cognitive_complexity = "allow" missing_const_for_fn = "allow" similar_names = "allow" struct_excessive_bools = "allow" too_many_arguments = "allow" too_many_lines = "allow" type_complexity = "allow" multiple_crate_versions = "allow" large_enum_variant = "allow" must_use_candidate = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" significant_drop_tightening = "allow" [package.metadata.deb] depends = "pinentry" license-file = ["LICENSE"] assets = [ ["target/release/rbw", "usr/bin/", "755"], ["target/release/rbw-agent", "usr/bin/", "755"], ["target/release/completion/bash", "usr/share/bash-completion/completions/rbw", "644"], ["target/release/completion/zsh", "usr/share/zsh/vendor-completions/_rbw", "644"], ["target/release/completion/fish", "usr/share/fish/completions/rbw.fish", "644"], ["README.md", "usr/share/doc/rbw/README", "644"], ] rbw-1.13.2/LICENSE000064400000000000000000000022071046102023000115100ustar 00000000000000This software is Copyright (c) 2021 by Jesse Luehrs. This is free software, licensed under: The MIT (X11) License The MIT License 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. rbw-1.13.2/README.md000064400000000000000000000152231046102023000117640ustar 00000000000000# rbw This is an unofficial command line client for [Bitwarden](https://bitwarden.com/). Although it does come with its own [command line client](https://help.bitwarden.com/article/cli/), this client is limited by being stateless - to use it, you're required to manually lock and unlock the client, and pass the temporary keys around in environment variables, which makes it very difficult to use. This client avoids this problem by maintaining a background process which is able to hold the keys in memory, similar to the way that `ssh-agent` or `gpg-agent` work. This allows the client to be used in a much simpler way, with the background agent taking care of maintaining the necessary state. ## Maintenance I consider `rbw` to be essentially feature-complete for me at this point. While I still use it on a daily basis, and will continue to fix regressions as they occur, I am unlikely to spend time implementing new features on my own. If you would like to see new functionality in `rbw`, I am more than happy to review and merge pull requests implementing those features. ## Installation ### Arch Linux `rbw` is available in the [extra repository](https://archlinux.org/packages/extra/x86_64/rbw/). Alternatively, you can install [`rbw-git`](https://aur.archlinux.org/packages/rbw-git/) from the AUR, which will always build from the latest master commit. ### Debian/Ubuntu You can download a Debian package from [https://git.tozt.net/rbw/releases/deb/ ](https://git.tozt.net/rbw/releases/deb/). The packages are signed by [`minisign`](https://github.com/jedisct1/minisign), and can be verified using the public key `RWTM0AZ5RpROOfAIWx1HvYQ6pw1+FKwN6526UFTKNImP/Hz3ynCFst3r`. ### Fedora/EPEL `rbw` is available in [Fedora and EPEL 9](https://bodhi.fedoraproject.org/updates/?packages=rust-rbw) (for RHEL and compatible distributions). You can install it using `sudo dnf install rbw`. ### Homebrew `rbw` is available in the [Homebrew repository](https://formulae.brew.sh/formula/rbw). You can install it via `brew install rbw`. ### Nix `rbw` is available in the [NixOS repository](https://search.nixos.org/packages?show=rbw). You can try it out via `nix-shell -p rbw`. ### Alpine `rbw` is available in the [testing repository](https://pkgs.alpinelinux.org/packages?name=rbw). If you are not using the `edge` version of alpine you have to [enable the repository manually](https://wiki.alpinelinux.org/wiki/Repositories#Testing). ### Other With a working Rust installation, `rbw` can be installed via `cargo install --locked rbw`. This requires that the [`pinentry`](https://www.gnupg.org/related_software/pinentry/index.en.html) program is installed (to display password prompts). ## Configuration Configuration options are set using the `rbw config` command. Available configuration options: * `email`: The email address to use as the account name when logging into the Bitwarden server. Required. * `sso_id`: The SSO organization ID. Defaults to regular login process if unset. * `base_url`: The URL of the Bitwarden server to use. Defaults to the official server at `https://api.bitwarden.com/` if unset. * `identity_url`: The URL of the Bitwarden identity server to use. If unset, will use the `/identity` path on the configured `base_url`, or `https://identity.bitwarden.com/` if no `base_url` is set. * `ui_url`: The URL of the Bitwarden UI to use. If unset, will default to `https://vault.bitwarden.com/`. * `notifications_url`: The URL of the Bitwarden notifications server to use. If unset, will use the `/notifications` path on the configured `base_url`, or `https://notifications.bitwarden.com/` if no `base_url` is set. * `lock_timeout`: The number of seconds to keep the master keys in memory for before requiring the password to be entered again. Defaults to `3600` (one hour). * `sync_interval`: `rbw` will automatically sync the database from the server at an interval of this many seconds, while the agent is running. Setting this value to `0` disables this behavior. Defaults to `3600` (one hour). * `pinentry`: The [pinentry](https://www.gnupg.org/related_software/pinentry/index.html) executable to use. Defaults to `pinentry`. ### Profiles `rbw` supports different configuration profiles, which can be switched between by using the `RBW_PROFILE` environment variable. Setting it to a name (for example, `RBW_PROFILE=work` or `RBW_PROFILE=personal`) can be used to switch between several different vaults - each will use its own separate configuration, local vault, and agent. ## Usage Commands can generally be used directly, and will handle logging in or unlocking as necessary. For instance, running `rbw ls` will run `rbw unlock` to unlock the password database before generating the list of entries (but will not attempt to log in to the server), `rbw sync` will automatically run `rbw login` to log in to the server before downloading the password database (but will not unlock the database), and `rbw add` will do both. Logging into the server and unlocking the database will only be done as necessary, so running `rbw login` when you are already logged in will do nothing, and similarly for `rbw unlock`. If necessary, you can explicitly log out by running `rbw purge`, and you can explicitly lock the database by running `rbw lock` or `rbw stop-agent`. `rbw help` can be used to get more information about the available functionality. Run `rbw get ` to get your passwords. If you also want to get the username or the note associated, you can use the flag `--full`. You can also use the flag `--field={field}` to get whatever default or custom field you want. The `--raw` flag will show the output as JSON. In addition to matching against the name, you can pass a UUID as the name to search for the entry with that id, or a URL to search for an entry with a matching website entry. *Note to users of the official Bitwarden server (at bitwarden.com)*: The official server has a tendency to detect command line traffic as bot traffic (see [this issue](https://github.com/bitwarden/cli/issues/383) for details). In order to use `rbw` with the official Bitwarden server, you will need to first run `rbw register` to register each device using `rbw` with the Bitwarden server. This will prompt you for your personal API key which you can find using the instructions [here](https://bitwarden.com/help/article/personal-api-key/). ## Related projects * [rofi-rbw](https://github.com/fdw/rofi-rbw): A rofi frontend for Bitwarden * [bw-ssh](https://framagit.org/Glandos/bw-ssh/): Manage SSH key passphrases in Bitwarden * [rbw-menu](https://github.com/rbuchberger/rbw-menu): Tiny menu picker for rbw * [ulauncher-rbw](https://0xacab.org/varac-projects/ulauncher-rbw): [Ulauncher](https://ulauncher.io/) rbw extension rbw-1.13.2/bin/git-credential-rbw000075500000000000000000000011221046102023000146570ustar 00000000000000#!/bin/sh set -f [ "$1" = get ] || exit while read -r line; do case $line in protocol=*) protocol=${line#*=} ;; host=*) host=${line#*=} ;; username=*) user=${line#*=} ;; esac done output= #shellcheck disable=2154 for arg in \ "${protocol:+$protocol://}$host" \ "$host" \ "${host2=${host%.*}}" \ "${host2#*.}" do # exit on first good result [ -n "$user" ] && output=$(rbw get --full "$arg" "$user") && break output=$(rbw get --full "$arg") && break done || exit printf '%s\n' "$output" | sed -n ' 1{ s/^/password=/p } s/^Username: /username=/p s/^URI: /host=/p ' rbw-1.13.2/bin/pass-import000075500000000000000000000016371046102023000134650ustar 00000000000000#!/bin/sh set -eu EDITOR=$(mktemp) trap 'rm -f $EDITOR' EXIT cat > "$EDITOR" <<'EOF' #!/bin/sh cat > "$1" EOF chmod 700 "$EDITOR" export EDITOR for entry in $(pass git ls-files | grep '\.gpg$' | sed 's/\.gpg$//'); do echo "$entry" pw=$(pass show "$entry") user="${entry##*/}" full_name="${entry%/*}" if echo "$full_name" | grep -q /; then name="${full_name##*/}" folder="${full_name%/*}" else name="$full_name" folder="" fi if echo "$name" | grep -q '\.'; then if [ -z "$folder" ]; then echo "$pw" | rbw add --uri "$name" "$name" "$user" else echo "$pw" | rbw add --uri "$name" --folder "$folder" "$name" "$user" fi else if [ -z "$folder" ]; then echo "$pw" | rbw add "$name" "$user" else echo "$pw" | rbw add --folder "$folder" "$name" "$user" fi fi done rbw-1.13.2/bin/rbw-fzf000075500000000000000000000005551046102023000125620ustar 00000000000000#!/bin/bash set -eu set -o pipefail rbw ls --fields name,user,folder | \ perl -plE'/^([^\t]*)\t([^\t]*)\t([^\t]*)$/; $_ = join("/", grep { length } ($3, $1, $2)) . "\0$_"' | \ sort | \ fzf --with-nth=1 -d '\x00' | \ perl -ple'/^([^\0]*)\0([^\t]*)\t([^\t]*)\t([^\t]*)$/; $_ = "$2 $3"; $_ .= " --folder=\"$4\"" if length $4' | \ xargs -r rbw get rbw-1.13.2/bin/rbw-pinentry-keyring000075500000000000000000000067231046102023000153160ustar 00000000000000#!/bin/bash [[ -z "${RBW_PROFILE}" ]] && rbw_profile='rbw' || rbw_profile="rbw-${RBW_PROFILE}" set -eEuo pipefail function help() { cat </dev/null 2>&1 fi fi printf 'D %s\n' "$secret_value" echo 'OK' else cmd="SETTITLE $title\n" cmd+="SETPROMPT $prompt\n" cmd+="SETDESC $desc\n" cmd+="GETPIN\n" secret_value="$(printf "$cmd" | pinentry "$@" | grep -E "^D " | cut -c3-)" printf 'D %s\n' "$secret_value" echo 'OK' fi ;; BYE) exit ;; *) echo 'ERR Unknown command' ;; esac done } command="$1" case "$command" in -h|--help|help) help ;; -c|--clear|clear) clear ;; *) getpin "$@" ;; esac rbw-1.13.2/bin/rbw-rofi000075500000000000000000000002631046102023000127300ustar 00000000000000#!/bin/bash set -eu set -o pipefail rbw ls --fields folder,name,user | sed 's/\t/\//g' | sort | rofi -dmenu | sed 's/^[^\/]*\///' | sed 's/\// /' | xargs -r rbw get | xclip -l 1 rbw-1.13.2/src/actions.rs000064400000000000000000000216351046102023000133060ustar 00000000000000use crate::prelude::*; pub async fn register( email: &str, apikey: crate::locked::ApiKey, ) -> Result<()> { let (client, config) = api_client_async().await?; client .register(email, &crate::config::device_id(&config).await?, &apikey) .await?; Ok(()) } pub async fn login( email: &str, password: crate::locked::Password, two_factor_token: Option<&str>, two_factor_provider: Option, ) -> Result<( String, String, crate::api::KdfType, u32, Option, Option, String, )> { let (client, config) = api_client_async().await?; let (kdf, iterations, memory, parallelism) = client.prelogin(email).await?; let identity = crate::identity::Identity::new( email, &password, kdf, iterations, memory, parallelism, )?; let (access_token, refresh_token, protected_key) = client .login( email, config.sso_id.as_deref(), &crate::config::device_id(&config).await?, &identity.master_password_hash, two_factor_token, two_factor_provider, ) .await?; Ok(( access_token, refresh_token, kdf, iterations, memory, parallelism, protected_key, )) } pub fn unlock( email: &str, password: &crate::locked::Password, kdf: crate::api::KdfType, iterations: u32, memory: Option, parallelism: Option, protected_key: &str, protected_private_key: &str, protected_org_keys: &std::collections::HashMap, ) -> Result<( crate::locked::Keys, std::collections::HashMap, )> { let identity = crate::identity::Identity::new( email, password, kdf, iterations, memory, parallelism, )?; let protected_key = crate::cipherstring::CipherString::new(protected_key)?; let key = match protected_key.decrypt_locked_symmetric(&identity.keys) { Ok(master_keys) => crate::locked::Keys::new(master_keys), Err(Error::InvalidMac) => { return Err(Error::IncorrectPassword { message: "Password is incorrect. Try again.".to_string(), }) } Err(e) => return Err(e), }; let protected_private_key = crate::cipherstring::CipherString::new(protected_private_key)?; let private_key = match protected_private_key.decrypt_locked_symmetric(&key) { Ok(private_key) => crate::locked::PrivateKey::new(private_key), Err(e) => return Err(e), }; let mut org_keys = std::collections::HashMap::new(); for (org_id, protected_org_key) in protected_org_keys { let protected_org_key = crate::cipherstring::CipherString::new(protected_org_key)?; let org_key = match protected_org_key.decrypt_locked_asymmetric(&private_key) { Ok(org_key) => crate::locked::Keys::new(org_key), Err(e) => return Err(e), }; org_keys.insert(org_id.to_string(), org_key); } Ok((key, org_keys)) } pub async fn sync( access_token: &str, refresh_token: &str, ) -> Result<( Option, ( String, String, std::collections::HashMap, Vec, ), )> { with_exchange_refresh_token_async( access_token, refresh_token, |access_token| { let access_token = access_token.to_string(); Box::pin(async move { sync_once(&access_token).await }) }, ) .await } async fn sync_once( access_token: &str, ) -> Result<( String, String, std::collections::HashMap, Vec, )> { let (client, _) = api_client_async().await?; client.sync(access_token).await } pub fn add( access_token: &str, refresh_token: &str, name: &str, data: &crate::db::EntryData, notes: Option<&str>, folder_id: Option<&str>, ) -> Result<(Option, ())> { with_exchange_refresh_token(access_token, refresh_token, |access_token| { add_once(access_token, name, data, notes, folder_id) }) } fn add_once( access_token: &str, name: &str, data: &crate::db::EntryData, notes: Option<&str>, folder_id: Option<&str>, ) -> Result<()> { let (client, _) = api_client()?; client.add(access_token, name, data, notes, folder_id)?; Ok(()) } pub fn edit( access_token: &str, refresh_token: &str, id: &str, org_id: Option<&str>, name: &str, data: &crate::db::EntryData, fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<(Option, ())> { with_exchange_refresh_token(access_token, refresh_token, |access_token| { edit_once( access_token, id, org_id, name, data, fields, notes, folder_uuid, history, ) }) } fn edit_once( access_token: &str, id: &str, org_id: Option<&str>, name: &str, data: &crate::db::EntryData, fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<()> { let (client, _) = api_client()?; client.edit( access_token, id, org_id, name, data, fields, notes, folder_uuid, history, )?; Ok(()) } pub fn remove( access_token: &str, refresh_token: &str, id: &str, ) -> Result<(Option, ())> { with_exchange_refresh_token(access_token, refresh_token, |access_token| { remove_once(access_token, id) }) } fn remove_once(access_token: &str, id: &str) -> Result<()> { let (client, _) = api_client()?; client.remove(access_token, id)?; Ok(()) } pub fn list_folders( access_token: &str, refresh_token: &str, ) -> Result<(Option, Vec<(String, String)>)> { with_exchange_refresh_token(access_token, refresh_token, |access_token| { list_folders_once(access_token) }) } fn list_folders_once(access_token: &str) -> Result> { let (client, _) = api_client()?; client.folders(access_token) } pub fn create_folder( access_token: &str, refresh_token: &str, name: &str, ) -> Result<(Option, String)> { with_exchange_refresh_token(access_token, refresh_token, |access_token| { create_folder_once(access_token, name) }) } fn create_folder_once(access_token: &str, name: &str) -> Result { let (client, _) = api_client()?; client.create_folder(access_token, name) } fn with_exchange_refresh_token( access_token: &str, refresh_token: &str, f: F, ) -> Result<(Option, T)> where F: Fn(&str) -> Result, { match f(access_token) { Ok(t) => Ok((None, t)), Err(Error::RequestUnauthorized) => { let access_token = exchange_refresh_token(refresh_token)?; let t = f(&access_token)?; Ok((Some(access_token), t)) } Err(e) => Err(e), } } async fn with_exchange_refresh_token_async( access_token: &str, refresh_token: &str, f: F, ) -> Result<(Option, T)> where F: Fn( &str, ) -> std::pin::Pin< Box> + Send>, > + Send + Sync, T: Send, { match f(access_token).await { Ok(t) => Ok((None, t)), Err(Error::RequestUnauthorized) => { let access_token = exchange_refresh_token_async(refresh_token).await?; let t = f(&access_token).await?; Ok((Some(access_token), t)) } Err(e) => Err(e), } } fn exchange_refresh_token(refresh_token: &str) -> Result { let (client, _) = api_client()?; client.exchange_refresh_token(refresh_token) } async fn exchange_refresh_token_async(refresh_token: &str) -> Result { let (client, _) = api_client()?; client.exchange_refresh_token_async(refresh_token).await } fn api_client() -> Result<(crate::api::Client, crate::config::Config)> { let config = crate::config::Config::load()?; let client = crate::api::Client::new( &config.base_url(), &config.identity_url(), &config.ui_url(), config.client_cert_path(), ); Ok((client, config)) } async fn api_client_async( ) -> Result<(crate::api::Client, crate::config::Config)> { let config = crate::config::Config::load_async().await?; let client = crate::api::Client::new( &config.base_url(), &config.identity_url(), &config.ui_url(), config.client_cert_path(), ); Ok((client, config)) } rbw-1.13.2/src/api.rs000064400000000000000000001525731046102023000124250ustar 00000000000000// serde_repr generates some as conversions that we can't seem to silence from // here, unfortunately #![allow(clippy::as_conversions)] use crate::prelude::*; use rand::Rng as _; use sha2::Digest as _; use tokio::io::AsyncReadExt as _; use crate::json::{ DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _, }; #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Copy, Clone, PartialEq, Eq, )] #[repr(u8)] pub enum UriMatchType { Domain = 0, Host = 1, StartsWith = 2, Exact = 3, RegularExpression = 4, Never = 5, } impl std::fmt::Display for UriMatchType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { #[allow(clippy::enum_glob_use)] use UriMatchType::*; let s = match self { Domain => "domain", Host => "host", StartsWith => "starts_with", Exact => "exact", RegularExpression => "regular_expression", Never => "never", }; write!(f, "{s}") } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TwoFactorProviderType { Authenticator = 0, Email = 1, Duo = 2, Yubikey = 3, U2f = 4, Remember = 5, OrganizationDuo = 6, WebAuthn = 7, } impl TwoFactorProviderType { pub fn message(&self) -> &str { match *self { Self::Authenticator => "Enter the 6 digit verification code from your authenticator app.", Self::Yubikey => "Insert your Yubikey and push the button.", Self::Email => "Enter the PIN you received via email.", _ => "Enter the code." } } pub fn header(&self) -> &str { match *self { Self::Authenticator => "Authenticator App", Self::Yubikey => "Yubikey", Self::Email => "Email Code", _ => "Two Factor Authentication", } } pub fn grab(&self) -> bool { !matches!(self, Self::Email) } } impl<'de> serde::Deserialize<'de> for TwoFactorProviderType { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { struct TwoFactorProviderTypeVisitor; impl serde::de::Visitor<'_> for TwoFactorProviderTypeVisitor { type Value = TwoFactorProviderType; fn expecting( &self, formatter: &mut std::fmt::Formatter, ) -> std::fmt::Result { formatter.write_str("two factor provider id") } fn visit_str( self, value: &str, ) -> std::result::Result where E: serde::de::Error, { value.parse().map_err(serde::de::Error::custom) } fn visit_u64( self, value: u64, ) -> std::result::Result where E: serde::de::Error, { std::convert::TryFrom::try_from(value) .map_err(serde::de::Error::custom) } } deserializer.deserialize_any(TwoFactorProviderTypeVisitor) } } impl std::convert::TryFrom for TwoFactorProviderType { type Error = Error; fn try_from(ty: u64) -> Result { match ty { 0 => Ok(Self::Authenticator), 1 => Ok(Self::Email), 2 => Ok(Self::Duo), 3 => Ok(Self::Yubikey), 4 => Ok(Self::U2f), 5 => Ok(Self::Remember), 6 => Ok(Self::OrganizationDuo), 7 => Ok(Self::WebAuthn), _ => Err(Error::InvalidTwoFactorProvider { ty: format!("{ty}"), }), } } } impl std::str::FromStr for TwoFactorProviderType { type Err = Error; fn from_str(ty: &str) -> Result { match ty { "0" => Ok(Self::Authenticator), "1" => Ok(Self::Email), "2" => Ok(Self::Duo), "3" => Ok(Self::Yubikey), "4" => Ok(Self::U2f), "5" => Ok(Self::Remember), "6" => Ok(Self::OrganizationDuo), "7" => Ok(Self::WebAuthn), _ => Err(Error::InvalidTwoFactorProvider { ty: ty.to_string() }), } } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum KdfType { Pbkdf2 = 0, Argon2id = 1, } impl<'de> serde::Deserialize<'de> for KdfType { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { struct KdfTypeVisitor; impl serde::de::Visitor<'_> for KdfTypeVisitor { type Value = KdfType; fn expecting( &self, formatter: &mut std::fmt::Formatter, ) -> std::fmt::Result { formatter.write_str("kdf id") } fn visit_str( self, value: &str, ) -> std::result::Result where E: serde::de::Error, { value.parse().map_err(serde::de::Error::custom) } fn visit_u64( self, value: u64, ) -> std::result::Result where E: serde::de::Error, { std::convert::TryFrom::try_from(value) .map_err(serde::de::Error::custom) } } deserializer.deserialize_any(KdfTypeVisitor) } } impl std::convert::TryFrom for KdfType { type Error = Error; fn try_from(ty: u64) -> Result { match ty { 0 => Ok(Self::Pbkdf2), 1 => Ok(Self::Argon2id), _ => Err(Error::InvalidKdfType { ty: format!("{ty}"), }), } } } impl std::str::FromStr for KdfType { type Err = Error; fn from_str(ty: &str) -> Result { match ty { "0" => Ok(Self::Pbkdf2), "1" => Ok(Self::Argon2id), _ => Err(Error::InvalidKdfType { ty: ty.to_string() }), } } } impl serde::Serialize for KdfType { fn serialize( &self, serializer: S, ) -> std::result::Result where S: serde::Serializer, { let s = match self { Self::Pbkdf2 => "0", Self::Argon2id => "1", }; serializer.serialize_str(s) } } #[derive(serde::Serialize, Debug)] struct PreloginReq { email: String, } #[derive(serde::Deserialize, Debug)] struct PreloginRes { #[serde(rename = "Kdf", alias = "kdf")] kdf: KdfType, #[serde(rename = "KdfIterations", alias = "kdfIterations")] kdf_iterations: u32, #[serde(rename = "KdfMemory", alias = "kdfMemory")] kdf_memory: Option, #[serde(rename = "KdfParallelism", alias = "kdfParallelism")] kdf_parallelism: Option, } #[derive(serde::Serialize, Debug)] struct ConnectTokenReq { grant_type: String, scope: String, client_id: String, #[serde(rename = "deviceType")] device_type: u32, #[serde(rename = "deviceIdentifier")] device_identifier: String, #[serde(rename = "deviceName")] device_name: String, #[serde(rename = "devicePushToken")] device_push_token: String, #[serde(rename = "twoFactorToken")] two_factor_token: Option, #[serde(rename = "twoFactorProvider")] two_factor_provider: Option, #[serde(flatten)] auth: ConnectTokenAuth, } #[derive(serde::Serialize, Debug)] #[serde(untagged)] enum ConnectTokenAuth { Password(ConnectTokenPassword), AuthCode(ConnectTokenAuthCode), ClientCredentials(ConnectTokenClientCredentials), } #[derive(serde::Serialize, Debug)] struct ConnectTokenPassword { username: String, password: String, } #[derive(serde::Serialize, Debug)] struct ConnectTokenAuthCode { code: String, code_verifier: String, redirect_uri: String, } #[derive(serde::Serialize, Debug)] struct ConnectTokenClientCredentials { username: String, client_secret: String, } #[derive(serde::Deserialize, Debug)] struct ConnectTokenRes { access_token: String, refresh_token: String, #[serde(rename = "Key", alias = "key")] key: String, } #[derive(serde::Deserialize, Debug)] struct ConnectErrorRes { error: String, error_description: Option, #[serde(rename = "ErrorModel", alias = "errorModel")] error_model: Option, #[serde(rename = "TwoFactorProviders", alias = "twoFactorProviders")] two_factor_providers: Option>, } #[derive(serde::Deserialize, Debug)] struct ConnectErrorResErrorModel { #[serde(rename = "Message", alias = "message")] message: String, } #[derive(serde::Serialize, Debug)] struct ConnectRefreshTokenReq { grant_type: String, client_id: String, refresh_token: String, } #[derive(serde::Deserialize, Debug)] struct ConnectRefreshTokenRes { access_token: String, } #[derive(serde::Deserialize, Debug)] struct SyncRes { #[serde(rename = "Ciphers", alias = "ciphers")] ciphers: Vec, #[serde(rename = "Profile", alias = "profile")] profile: SyncResProfile, #[serde(rename = "Folders", alias = "folders")] folders: Vec, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResCipher { #[serde(rename = "Id", alias = "id")] id: String, #[serde(rename = "FolderId", alias = "folderId")] folder_id: Option, #[serde(rename = "OrganizationId", alias = "organizationId")] organization_id: Option, #[serde(rename = "Name", alias = "name")] name: String, #[serde(rename = "Login", alias = "login")] login: Option, #[serde(rename = "Card", alias = "card")] card: Option, #[serde(rename = "Identity", alias = "identity")] identity: Option, #[serde(rename = "SecureNote", alias = "secureNote")] secure_note: Option, #[serde(rename = "Notes", alias = "notes")] notes: Option, #[serde(rename = "PasswordHistory", alias = "passwordHistory")] password_history: Option>, #[serde(rename = "Fields", alias = "fields")] fields: Option>, #[serde(rename = "DeletedDate", alias = "deletedDate")] deleted_date: Option, #[serde(rename = "Key", alias = "key")] key: Option, } impl SyncResCipher { fn to_entry( &self, folders: &[SyncResFolder], ) -> Option { if self.deleted_date.is_some() { return None; } let history = self.password_history .as_ref() .map_or_else(Vec::new, |history| { history .iter() .filter_map(|entry| { // Gets rid of entries with a non-existent // password entry.password.clone().map(|p| { crate::db::HistoryEntry { last_used_date: entry .last_used_date .clone(), password: p, } }) }) .collect() }); let (folder, folder_id) = self.folder_id.as_ref().map_or((None, None), |folder_id| { let mut folder_name = None; for folder in folders { if &folder.id == folder_id { folder_name = Some(folder.name.clone()); } } (folder_name, Some(folder_id)) }); let data = if let Some(login) = &self.login { crate::db::EntryData::Login { username: login.username.clone(), password: login.password.clone(), totp: login.totp.clone(), uris: login.uris.as_ref().map_or_else( std::vec::Vec::new, |uris| { uris.iter() .filter_map(|uri| { uri.uri.clone().map(|s| crate::db::Uri { uri: s, match_type: uri.match_type, }) }) .collect() }, ), } } else if let Some(card) = &self.card { crate::db::EntryData::Card { cardholder_name: card.cardholder_name.clone(), number: card.number.clone(), brand: card.brand.clone(), exp_month: card.exp_month.clone(), exp_year: card.exp_year.clone(), code: card.code.clone(), } } else if let Some(identity) = &self.identity { crate::db::EntryData::Identity { title: identity.title.clone(), first_name: identity.first_name.clone(), middle_name: identity.middle_name.clone(), last_name: identity.last_name.clone(), address1: identity.address1.clone(), address2: identity.address2.clone(), address3: identity.address3.clone(), city: identity.city.clone(), state: identity.state.clone(), postal_code: identity.postal_code.clone(), country: identity.country.clone(), phone: identity.phone.clone(), email: identity.email.clone(), ssn: identity.ssn.clone(), license_number: identity.license_number.clone(), passport_number: identity.passport_number.clone(), username: identity.username.clone(), } } else if let Some(_secure_note) = &self.secure_note { crate::db::EntryData::SecureNote } else { return None; }; let fields = self.fields.as_ref().map_or_else(Vec::new, |fields| { fields .iter() .map(|field| crate::db::Field { ty: field.ty, name: field.name.clone(), value: field.value.clone(), linked_id: field.linked_id, }) .collect() }); Some(crate::db::Entry { id: self.id.clone(), org_id: self.organization_id.clone(), folder, folder_id: folder_id.map(std::string::ToString::to_string), name: self.name.clone(), data, fields, notes: self.notes.clone(), history, key: self.key.clone(), }) } } #[derive(serde::Deserialize, Debug)] struct SyncResProfile { #[serde(rename = "Key", alias = "key")] key: String, #[serde(rename = "PrivateKey", alias = "privateKey")] private_key: String, #[serde(rename = "Organizations", alias = "organizations")] organizations: Vec, } #[derive(serde::Deserialize, Debug)] struct SyncResProfileOrganization { #[serde(rename = "Id", alias = "id")] id: String, #[serde(rename = "Key", alias = "key")] key: String, } #[derive(serde::Deserialize, Debug, Clone)] struct SyncResFolder { #[serde(rename = "Id", alias = "id")] id: String, #[serde(rename = "Name", alias = "name")] name: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherLogin { #[serde(rename = "Username", alias = "username")] username: Option, #[serde(rename = "Password", alias = "password")] password: Option, #[serde(rename = "Totp", alias = "totp")] totp: Option, #[serde(rename = "Uris", alias = "uris")] uris: Option>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherLoginUri { #[serde(rename = "Uri", alias = "uri")] uri: Option, #[serde(rename = "Match", alias = "match")] match_type: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherCard { #[serde(rename = "CardholderName", alias = "cardholderName")] cardholder_name: Option, #[serde(rename = "Number", alias = "number")] number: Option, #[serde(rename = "Brand", alias = "brand")] brand: Option, #[serde(rename = "ExpMonth", alias = "expMonth")] exp_month: Option, #[serde(rename = "ExpYear", alias = "expYear")] exp_year: Option, #[serde(rename = "Code", alias = "code")] code: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherIdentity { #[serde(rename = "Title", alias = "title")] title: Option, #[serde(rename = "FirstName", alias = "firstName")] first_name: Option, #[serde(rename = "MiddleName", alias = "middleName")] middle_name: Option, #[serde(rename = "LastName", alias = "lastName")] last_name: Option, #[serde(rename = "Address1", alias = "address1")] address1: Option, #[serde(rename = "Address2", alias = "address2")] address2: Option, #[serde(rename = "Address3", alias = "address3")] address3: Option, #[serde(rename = "City", alias = "city")] city: Option, #[serde(rename = "State", alias = "state")] state: Option, #[serde(rename = "PostalCode", alias = "postalCode")] postal_code: Option, #[serde(rename = "Country", alias = "country")] country: Option, #[serde(rename = "Phone", alias = "phone")] phone: Option, #[serde(rename = "Email", alias = "email")] email: Option, #[serde(rename = "SSN", alias = "ssn")] ssn: Option, #[serde(rename = "LicenseNumber", alias = "licenseNumber")] license_number: Option, #[serde(rename = "PassportNumber", alias = "passportNumber")] passport_number: Option, #[serde(rename = "Username", alias = "username")] username: Option, } #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Clone, Copy, PartialEq, Eq, )] #[repr(u16)] pub enum FieldType { Text = 0, Hidden = 1, Boolean = 2, Linked = 3, } #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Clone, Copy, PartialEq, Eq, )] #[repr(u16)] pub enum LinkedIdType { LoginUsername = 100, LoginPassword = 101, CardCardholderName = 300, CardExpMonth = 301, CardExpYear = 302, CardCode = 303, CardBrand = 304, CardNumber = 305, IdentityTitle = 400, IdentityMiddleName = 401, IdentityAddress1 = 402, IdentityAddress2 = 403, IdentityAddress3 = 404, IdentityCity = 405, IdentityState = 406, IdentityPostalCode = 407, IdentityCountry = 408, IdentityCompany = 409, IdentityEmail = 410, IdentityPhone = 411, IdentitySsn = 412, IdentityUsername = 413, IdentityPassportNumber = 414, IdentityLicenseNumber = 415, IdentityFirstName = 416, IdentityLastName = 417, IdentityFullName = 418, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherField { #[serde(rename = "Type", alias = "type")] ty: Option, #[serde(rename = "Name", alias = "name")] name: Option, #[serde(rename = "Value", alias = "value")] value: Option, #[serde(rename = "LinkedId", alias = "linkedId")] linked_id: Option, } // this is just a name and some notes, both of which are already on the cipher // object #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherSecureNote {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResPasswordHistory { #[serde(rename = "LastUsedDate", alias = "lastUsedDate")] last_used_date: String, #[serde(rename = "Password", alias = "password")] password: Option, } #[derive(serde::Serialize, Debug)] struct CiphersPostReq { #[serde(rename = "type")] ty: u32, // XXX what are the valid types? #[serde(rename = "folderId")] folder_id: Option, name: String, notes: Option, login: Option, card: Option, identity: Option, #[serde(rename = "secureNote")] secure_note: Option, } #[derive(serde::Serialize, Debug)] struct CiphersPutReq { #[serde(rename = "type")] ty: u32, // XXX what are the valid types? #[serde(rename = "folderId")] folder_id: Option, #[serde(rename = "organizationId")] organization_id: Option, name: String, notes: Option, login: Option, card: Option, identity: Option, fields: Vec, #[serde(rename = "secureNote")] secure_note: Option, #[serde(rename = "passwordHistory")] password_history: Vec, } #[derive(serde::Serialize, Debug)] struct CiphersPutReqHistory { #[serde(rename = "LastUsedDate")] last_used_date: String, #[serde(rename = "Password")] password: String, } #[derive(serde::Deserialize, Debug)] struct FoldersRes { #[serde(rename = "Data", alias = "data")] data: Vec, } #[derive(serde::Deserialize, Debug)] struct FoldersResData { #[serde(rename = "Id", alias = "id")] id: String, #[serde(rename = "Name", alias = "name")] name: String, } #[derive(serde::Serialize, Debug)] struct FoldersPostReq { name: String, } // Used for the Bitwarden-Client-Name header. Accepted values: // https://github.com/bitwarden/server/blob/main/src/Core/Enums/BitwardenClient.cs const BITWARDEN_CLIENT: &str = "cli"; // DeviceType.LinuxDesktop, as per Bitwarden API device types. const DEVICE_TYPE: u8 = 8; #[derive(Debug)] pub struct Client { base_url: String, identity_url: String, ui_url: String, client_cert_path: Option, } impl Client { pub fn new( base_url: &str, identity_url: &str, ui_url: &str, client_cert_path: Option<&std::path::Path>, ) -> Self { Self { base_url: base_url.to_string(), identity_url: identity_url.to_string(), ui_url: ui_url.to_string(), client_cert_path: client_cert_path .map(std::path::Path::to_path_buf), } } async fn reqwest_client(&self) -> Result { let mut default_headers = axum::http::HeaderMap::new(); default_headers.insert( "Bitwarden-Client-Name", axum::http::HeaderValue::from_static(BITWARDEN_CLIENT), ); default_headers.insert( "Bitwarden-Client-Version", axum::http::HeaderValue::from_static(env!("CARGO_PKG_VERSION")), ); default_headers.append( "Device-Type", // unwrap is safe here because DEVICE_TYPE is a number and digits // are valid ASCII axum::http::HeaderValue::from_str(&DEVICE_TYPE.to_string()) .unwrap(), ); let user_agent = format!( "{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") ); if let Some(client_cert_path) = self.client_cert_path.as_ref() { let mut buf = Vec::new(); let mut f = tokio::fs::File::open(client_cert_path) .await .map_err(|e| Error::LoadClientCert { source: e, file: client_cert_path.clone(), })?; f.read_to_end(&mut buf).await.map_err(|e| { Error::LoadClientCert { source: e, file: client_cert_path.clone(), } })?; let pem = reqwest::Identity::from_pem(&buf) .map_err(|e| Error::CreateReqwestClient { source: e })?; Ok(reqwest::Client::builder() .user_agent(user_agent) .identity(pem) .default_headers(default_headers) .build() .map_err(|e| Error::CreateReqwestClient { source: e })?) } else { Ok(reqwest::Client::builder() .user_agent(user_agent) .default_headers(default_headers) .build() .map_err(|e| Error::CreateReqwestClient { source: e })?) } } pub async fn prelogin( &self, email: &str, ) -> Result<(KdfType, u32, Option, Option)> { let prelogin = PreloginReq { email: email.to_string(), }; let client = self.reqwest_client().await?; let res = client .post(self.identity_url("/accounts/prelogin")) .json(&prelogin) .send() .await .map_err(|source| Error::Reqwest { source })?; let prelogin_res: PreloginRes = res.json_with_path().await?; Ok(( prelogin_res.kdf, prelogin_res.kdf_iterations, prelogin_res.kdf_memory, prelogin_res.kdf_parallelism, )) } pub async fn register( &self, email: &str, device_id: &str, apikey: &crate::locked::ApiKey, ) -> Result<()> { let connect_req = ConnectTokenReq { auth: ConnectTokenAuth::ClientCredentials( ConnectTokenClientCredentials { username: email.to_string(), client_secret: String::from_utf8( apikey.client_secret().to_vec(), ) .unwrap(), }, ), grant_type: "client_credentials".to_string(), scope: "api".to_string(), // XXX unwraps here are not necessarily safe client_id: String::from_utf8(apikey.client_id().to_vec()) .unwrap(), device_type: u32::from(DEVICE_TYPE), device_identifier: device_id.to_string(), device_name: "rbw".to_string(), device_push_token: String::new(), two_factor_token: None, two_factor_provider: None, }; let client = self.reqwest_client().await?; let res = client .post(self.identity_url("/connect/token")) .form(&connect_req) .send() .await .map_err(|source| Error::Reqwest { source })?; if res.status() == reqwest::StatusCode::OK { Ok(()) } else { let code = res.status().as_u16(); match res.text().await { Ok(body) => match body.clone().json_with_path() { Ok(json) => Err(classify_login_error(&json, code)), Err(e) => { log::warn!("{e}: {body}"); Err(Error::RequestFailed { status: code }) } }, Err(e) => { log::warn!("failed to read response body: {e}"); Err(Error::RequestFailed { status: code }) } } } } pub async fn login( &self, email: &str, sso_id: Option<&str>, device_id: &str, password_hash: &crate::locked::PasswordHash, two_factor_token: Option<&str>, two_factor_provider: Option, ) -> Result<(String, String, String)> { let connect_req = match sso_id { Some(sso_id) => { let (sso_code, sso_code_verifier, callback_url) = self.obtain_sso_code(sso_id).await?; ConnectTokenReq { auth: ConnectTokenAuth::AuthCode(ConnectTokenAuthCode { code: sso_code, code_verifier: sso_code_verifier, redirect_uri: callback_url, }), grant_type: "authorization_code".to_string(), scope: "api offline_access".to_string(), client_id: "cli".to_string(), device_type: u32::from(DEVICE_TYPE), device_identifier: device_id.to_string(), device_name: "rbw".to_string(), device_push_token: String::new(), two_factor_token: two_factor_token .map(std::string::ToString::to_string), two_factor_provider: two_factor_provider .map(|ty| ty as u32), } } None => ConnectTokenReq { auth: ConnectTokenAuth::Password(ConnectTokenPassword { username: email.to_string(), password: crate::base64::encode(password_hash.hash()), }), grant_type: "password".to_string(), scope: "api offline_access".to_string(), client_id: "cli".to_string(), device_type: 8, device_identifier: device_id.to_string(), device_name: "rbw".to_string(), device_push_token: String::new(), two_factor_token: two_factor_token .map(std::string::ToString::to_string), two_factor_provider: two_factor_provider.map(|ty| ty as u32), }, }; let client = self.reqwest_client().await?; let res = client .post(self.identity_url("/connect/token")) .form(&connect_req) .header( "auth-email", crate::base64::encode_url_safe_no_pad(email), ) .send() .await .map_err(|source| Error::Reqwest { source })?; if res.status() == reqwest::StatusCode::OK { let connect_res: ConnectTokenRes = res.json_with_path().await?; Ok(( connect_res.access_token, connect_res.refresh_token, connect_res.key, )) } else { let code = res.status().as_u16(); match res.text().await { Ok(body) => match body.clone().json_with_path() { Ok(json) => Err(classify_login_error(&json, code)), Err(e) => { log::warn!("{e}: {body}"); Err(Error::RequestFailed { status: code }) } }, Err(e) => { log::warn!("failed to read response body: {e}"); Err(Error::RequestFailed { status: code }) } } } } async fn obtain_sso_code( &self, sso_id: &str, ) -> Result<(String, String, String)> { let state: String = rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .take(64) .map(char::from) .collect(); let sso_code_verifier: String = rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .take(64) .map(char::from) .collect(); let mut hasher = sha2::Sha256::new(); hasher.update(sso_code_verifier.clone()); let code_challenge = crate::base64::encode_url_safe_no_pad(hasher.finalize()); let port = find_free_port(8065, 8070).await?; let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)) .await .map_err(|e| Error::CreateSSOCallbackServer { err: e })?; let callback_server = start_sso_callback_server(listener, state.as_str()); let callback_url = "http://localhost:".to_string() + port.to_string().as_str(); open::that( self.ui_url.clone() + "/#/sso?clientId=" + "cli" + "&redirectUri=" + urlencoding::encode(callback_url.as_str()) .into_owned() .as_str() + "&state=" + state.as_str() + "&codeChallenge=" + code_challenge.as_str() + "&identifier=" + sso_id, ) .map_err(|e| Error::FailedToOpenWebBrowser { err: e })?; // TODO: probably it'd be better to display the URL in the console if the automatic // open operation fails, instead of failing the whole process? E.g. docker container // case let sso_code = callback_server.await?; Ok((sso_code, sso_code_verifier, callback_url.to_string())) } pub async fn sync( &self, access_token: &str, ) -> Result<( String, String, std::collections::HashMap, Vec, )> { let client = self.reqwest_client().await?; let res = client .get(self.api_url("/sync")) .header("Authorization", format!("Bearer {access_token}")) .send() .await .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => { let sync_res: SyncRes = res.json_with_path().await?; let folders = sync_res.folders.clone(); let ciphers = sync_res .ciphers .iter() .filter_map(|cipher| cipher.to_entry(&folders)) .collect(); let org_keys = sync_res .profile .organizations .iter() .map(|org| (org.id.clone(), org.key.clone())) .collect(); Ok(( sync_res.profile.key, sync_res.profile.private_key, org_keys, ciphers, )) } reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn add( &self, access_token: &str, name: &str, data: &crate::db::EntryData, notes: Option<&str>, folder_id: Option<&str>, ) -> Result<()> { let mut req = CiphersPostReq { ty: 1, folder_id: folder_id.map(std::string::ToString::to_string), name: name.to_string(), notes: notes.map(std::string::ToString::to_string), login: None, card: None, identity: None, secure_note: None, }; match data { crate::db::EntryData::Login { username, password, totp, uris, } => { let uris = if uris.is_empty() { None } else { Some( uris.iter() .map(|s| CipherLoginUri { uri: Some(s.uri.to_string()), match_type: s.match_type, }) .collect(), ) }; req.login = Some(CipherLogin { username: username.clone(), password: password.clone(), totp: totp.clone(), uris, }); } crate::db::EntryData::Card { cardholder_name, number, brand, exp_month, exp_year, code, } => { req.card = Some(CipherCard { cardholder_name: cardholder_name.clone(), number: number.clone(), brand: brand.clone(), exp_month: exp_month.clone(), exp_year: exp_year.clone(), code: code.clone(), }); } crate::db::EntryData::Identity { title, first_name, middle_name, last_name, address1, address2, address3, city, state, postal_code, country, phone, email, ssn, license_number, passport_number, username, } => { req.identity = Some(CipherIdentity { title: title.clone(), first_name: first_name.clone(), middle_name: middle_name.clone(), last_name: last_name.clone(), address1: address1.clone(), address2: address2.clone(), address3: address3.clone(), city: city.clone(), state: state.clone(), postal_code: postal_code.clone(), country: country.clone(), phone: phone.clone(), email: email.clone(), ssn: ssn.clone(), license_number: license_number.clone(), passport_number: passport_number.clone(), username: username.clone(), }); } crate::db::EntryData::SecureNote {} => { req.secure_note = Some(CipherSecureNote {}); } } let client = reqwest::blocking::Client::new(); let res = client .post(self.api_url("/ciphers")) .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => Ok(()), reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn edit( &self, access_token: &str, id: &str, org_id: Option<&str>, name: &str, data: &crate::db::EntryData, fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<()> { let mut req = CiphersPutReq { ty: match data { crate::db::EntryData::Login { .. } => 1, crate::db::EntryData::SecureNote { .. } => 2, crate::db::EntryData::Card { .. } => 3, crate::db::EntryData::Identity { .. } => 4, }, folder_id: folder_uuid.map(std::string::ToString::to_string), organization_id: org_id.map(std::string::ToString::to_string), name: name.to_string(), notes: notes.map(std::string::ToString::to_string), login: None, card: None, identity: None, secure_note: None, fields: fields .iter() .map(|field| CipherField { ty: field.ty, name: field.name.clone(), value: field.value.clone(), linked_id: field.linked_id, }) .collect(), password_history: history .iter() .map(|entry| CiphersPutReqHistory { last_used_date: entry.last_used_date.clone(), password: entry.password.clone(), }) .collect(), }; match data { crate::db::EntryData::Login { username, password, totp, uris, } => { let uris = if uris.is_empty() { None } else { Some( uris.iter() .map(|s| CipherLoginUri { uri: Some(s.uri.to_string()), match_type: s.match_type, }) .collect(), ) }; req.login = Some(CipherLogin { username: username.clone(), password: password.clone(), totp: totp.clone(), uris, }); } crate::db::EntryData::Card { cardholder_name, number, brand, exp_month, exp_year, code, } => { req.card = Some(CipherCard { cardholder_name: cardholder_name.clone(), number: number.clone(), brand: brand.clone(), exp_month: exp_month.clone(), exp_year: exp_year.clone(), code: code.clone(), }); } crate::db::EntryData::Identity { title, first_name, middle_name, last_name, address1, address2, address3, city, state, postal_code, country, phone, email, ssn, license_number, passport_number, username, } => { req.identity = Some(CipherIdentity { title: title.clone(), first_name: first_name.clone(), middle_name: middle_name.clone(), last_name: last_name.clone(), address1: address1.clone(), address2: address2.clone(), address3: address3.clone(), city: city.clone(), state: state.clone(), postal_code: postal_code.clone(), country: country.clone(), phone: phone.clone(), email: email.clone(), ssn: ssn.clone(), license_number: license_number.clone(), passport_number: passport_number.clone(), username: username.clone(), }); } crate::db::EntryData::SecureNote {} => { req.secure_note = Some(CipherSecureNote {}); } } let client = reqwest::blocking::Client::new(); let res = client .put(self.api_url(&format!("/ciphers/{id}"))) .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => Ok(()), reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn remove(&self, access_token: &str, id: &str) -> Result<()> { let client = reqwest::blocking::Client::new(); let res = client .delete(self.api_url(&format!("/ciphers/{id}"))) .header("Authorization", format!("Bearer {access_token}")) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => Ok(()), reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn folders( &self, access_token: &str, ) -> Result> { let client = reqwest::blocking::Client::new(); let res = client .get(self.api_url("/folders")) .header("Authorization", format!("Bearer {access_token}")) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => { let folders_res: FoldersRes = res.json_with_path()?; Ok(folders_res .data .iter() .map(|folder| (folder.id.clone(), folder.name.clone())) .collect()) } reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn create_folder( &self, access_token: &str, name: &str, ) -> Result { let req = FoldersPostReq { name: name.to_string(), }; let client = reqwest::blocking::Client::new(); let res = client .post(self.api_url("/folders")) .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => { let folders_res: FoldersResData = res.json_with_path()?; Ok(folders_res.id) } reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn exchange_refresh_token( &self, refresh_token: &str, ) -> Result { let connect_req = ConnectRefreshTokenReq { grant_type: "refresh_token".to_string(), client_id: "cli".to_string(), refresh_token: refresh_token.to_string(), }; let client = reqwest::blocking::Client::new(); let res = client .post(self.identity_url("/connect/token")) .form(&connect_req) .send() .map_err(|source| Error::Reqwest { source })?; let connect_res: ConnectRefreshTokenRes = res.json_with_path()?; Ok(connect_res.access_token) } pub async fn exchange_refresh_token_async( &self, refresh_token: &str, ) -> Result { let connect_req = ConnectRefreshTokenReq { grant_type: "refresh_token".to_string(), client_id: "cli".to_string(), refresh_token: refresh_token.to_string(), }; let client = self.reqwest_client().await?; let res = client .post(self.identity_url("/connect/token")) .form(&connect_req) .send() .await .map_err(|source| Error::Reqwest { source })?; let connect_res: ConnectRefreshTokenRes = res.json_with_path().await?; Ok(connect_res.access_token) } fn api_url(&self, path: &str) -> String { format!("{}{}", self.base_url, path) } fn identity_url(&self, path: &str) -> String { format!("{}{}", self.identity_url, path) } } async fn find_free_port(bottom: u16, top: u16) -> Result { for port in bottom..top { if tokio::net::TcpListener::bind(("127.0.0.1", port)) .await .is_ok() { return Ok(port); } } Err(Error::FailedToFindFreePort { range: format!("({bottom}..{top})"), }) } #[derive(Clone)] struct SSOHandlerState { state: String, sender: tokio::sync::mpsc::Sender>, } async fn start_sso_callback_server( listener: tokio::net::TcpListener, state: &str, ) -> Result { let (shut_sender, shut_receiver) = tokio::sync::mpsc::channel(1); let (sender, mut receiver) = tokio::sync::mpsc::channel(1); let sso_handler_state = std::sync::Arc::new(SSOHandlerState { state: state.to_string(), sender: shut_sender, }); let app = axum::Router::new() .route("/", axum::routing::get(handle_sso_callback)) .with_state(sso_handler_state); axum::serve(listener, app) .with_graceful_shutdown(sso_server_graceful_shutdown( sender, shut_receiver, )) .await .map_err(|e| Error::FailedToProcessSSOCallback { msg: e.to_string(), })?; receiver.recv().await.unwrap() } async fn sso_server_graceful_shutdown( sender: tokio::sync::mpsc::Sender>, mut receiver: tokio::sync::mpsc::Receiver>, ) { sender.send(receiver.recv().await.unwrap()).await.unwrap(); } async fn handle_sso_callback( axum::extract::State(state): axum::extract::State< std::sync::Arc, >, axum::extract::Query(params): axum::extract::Query< std::collections::HashMap, >, ) -> axum::http::Response { match sso_query_code(¶ms, state.state.as_str()) { Ok(sso_code) => { state.sender.send(Ok(sso_code)).await.unwrap(); axum::http::Response::builder().status(axum::http::StatusCode::OK). body( "Success | rbw \

Successfully authenticated with rbw

\

You may now close this tab and return to the terminal.

\ ".to_string()).unwrap() } Err(e) => { state.sender.send(Err(e)).await.unwrap(); axum::http::Response::builder().status(axum::http::StatusCode::BAD_REQUEST). body( "Failed | rbw \

Something went wrong logging into the rbw

\

You may now close this tab and return to the terminal.

\ ".to_string()).unwrap() } } } fn sso_query_code( params: &std::collections::HashMap, state: &str, ) -> Result { let sso_code = params .get("code") .ok_or(Error::FailedToProcessSSOCallback { msg: "Could not obtain code from the URL".to_string(), })?; let received_state = params .get("state") .ok_or(Error::FailedToProcessSSOCallback { msg: "Could not obtain state from the URL".to_string(), })?; if received_state.split("_identifier=").next().unwrap() != state { return Err(Error::FailedToProcessSSOCallback { msg: format!("SSO callback states do not match, sent: {state}, received: {received_state}"), }); } Ok(sso_code.to_string()) } fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error { let error_desc = error_res.error_description.clone(); let error_desc = error_desc.as_deref(); match error_res.error.as_str() { "invalid_grant" => match error_desc { Some("invalid_username_or_password") => { if let Some(error_model) = error_res.error_model.as_ref() { let message = error_model.message.as_str().to_string(); return Error::IncorrectPassword { message }; } } Some("Two factor required.") => { if let Some(providers) = error_res.two_factor_providers.as_ref() { return Error::TwoFactorRequired { providers: providers.clone(), }; } } Some("Captcha required.") => { return Error::RegistrationRequired; } _ => {} }, "invalid_client" => { return Error::IncorrectApiKey; } "" => { // bitwarden_rs returns an empty error and error_description for // this case, for some reason if error_desc.is_none() || error_desc == Some("") { if let Some(error_model) = error_res.error_model.as_ref() { let message = error_model.message.as_str().to_string(); match message.as_str() { "Username or password is incorrect. Try again" | "TOTP code is not a number" => { return Error::IncorrectPassword { message }; } s => { if s.starts_with( "Invalid TOTP code! Server time: ", ) { return Error::IncorrectPassword { message }; } } } } } } _ => {} } log::warn!("unexpected error received during login: {:?}", error_res); Error::RequestFailed { status: code } } rbw-1.13.2/src/base64.rs000064400000000000000000000006561046102023000127320ustar 00000000000000use base64::Engine as _; pub fn encode>(input: T) -> String { base64::engine::general_purpose::STANDARD.encode(input) } pub fn encode_url_safe_no_pad>(input: T) -> String { base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input) } pub fn decode>( input: T, ) -> Result, base64::DecodeError> { base64::engine::general_purpose::STANDARD.decode(input) } rbw-1.13.2/src/bin/rbw/actions.rs000064400000000000000000000140611046102023000146430ustar 00000000000000use std::{io::Read as _, os::unix::ffi::OsStringExt as _}; use anyhow::Context as _; pub fn register() -> anyhow::Result<()> { simple_action(rbw::protocol::Action::Register) } pub fn login() -> anyhow::Result<()> { simple_action(rbw::protocol::Action::Login) } pub fn unlock() -> anyhow::Result<()> { simple_action(rbw::protocol::Action::Unlock) } pub fn unlocked() -> anyhow::Result<()> { match crate::sock::Sock::connect() { Ok(mut sock) => { sock.send(&rbw::protocol::Request::new( get_environment(), rbw::protocol::Action::CheckLock, ))?; let res = sock.recv()?; match res { rbw::protocol::Response::Ack => Ok(()), rbw::protocol::Response::Error { error } => { Err(anyhow::anyhow!("{}", error)) } _ => Err(anyhow::anyhow!("unexpected message: {:?}", res)), } } Err(e) => { if matches!( e.kind(), std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound ) { anyhow::bail!("agent not running"); } Err(e.into()) } } } pub fn sync() -> anyhow::Result<()> { simple_action(rbw::protocol::Action::Sync) } pub fn lock() -> anyhow::Result<()> { simple_action(rbw::protocol::Action::Lock) } pub fn quit() -> anyhow::Result<()> { match crate::sock::Sock::connect() { Ok(mut sock) => { let pidfile = rbw::dirs::pid_file(); let mut pid = String::new(); std::fs::File::open(pidfile)?.read_to_string(&mut pid)?; let Some(pid) = rustix::process::Pid::from_raw(pid.trim_end().parse()?) else { anyhow::bail!("failed to read pid from pidfile"); }; sock.send(&rbw::protocol::Request::new( get_environment(), rbw::protocol::Action::Quit, ))?; wait_for_exit(pid); Ok(()) } Err(e) => match e.kind() { // if the socket doesn't exist, or the socket exists but nothing // is listening on it, the agent must already be not running std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound => Ok(()), _ => Err(e.into()), }, } } pub fn decrypt( cipherstring: &str, entry_key: Option<&str>, org_id: Option<&str>, ) -> anyhow::Result { let mut sock = connect()?; sock.send(&rbw::protocol::Request::new( get_environment(), rbw::protocol::Action::Decrypt { cipherstring: cipherstring.to_string(), entry_key: entry_key.map(std::string::ToString::to_string), org_id: org_id.map(std::string::ToString::to_string), }, ))?; let res = sock.recv()?; match res { rbw::protocol::Response::Decrypt { plaintext } => Ok(plaintext), rbw::protocol::Response::Error { error } => { Err(anyhow::anyhow!("failed to decrypt: {}", error)) } _ => Err(anyhow::anyhow!("unexpected message: {:?}", res)), } } pub fn encrypt( plaintext: &str, org_id: Option<&str>, ) -> anyhow::Result { let mut sock = connect()?; sock.send(&rbw::protocol::Request::new( get_environment(), rbw::protocol::Action::Encrypt { plaintext: plaintext.to_string(), org_id: org_id.map(std::string::ToString::to_string), }, ))?; let res = sock.recv()?; match res { rbw::protocol::Response::Encrypt { cipherstring } => Ok(cipherstring), rbw::protocol::Response::Error { error } => { Err(anyhow::anyhow!("failed to encrypt: {}", error)) } _ => Err(anyhow::anyhow!("unexpected message: {:?}", res)), } } pub fn clipboard_store(text: &str) -> anyhow::Result<()> { simple_action(rbw::protocol::Action::ClipboardStore { text: text.to_string(), }) } pub fn version() -> anyhow::Result { let mut sock = connect()?; sock.send(&rbw::protocol::Request::new( get_environment(), rbw::protocol::Action::Version, ))?; let res = sock.recv()?; match res { rbw::protocol::Response::Version { version } => Ok(version), rbw::protocol::Response::Error { error } => { Err(anyhow::anyhow!("failed to get version: {}", error)) } _ => Err(anyhow::anyhow!("unexpected message: {:?}", res)), } } fn simple_action(action: rbw::protocol::Action) -> anyhow::Result<()> { let mut sock = connect()?; sock.send(&rbw::protocol::Request::new(get_environment(), action))?; let res = sock.recv()?; match res { rbw::protocol::Response::Ack => Ok(()), rbw::protocol::Response::Error { error } => { Err(anyhow::anyhow!("{}", error)) } _ => Err(anyhow::anyhow!("unexpected message: {:?}", res)), } } fn connect() -> anyhow::Result { crate::sock::Sock::connect().with_context(|| { let log = rbw::dirs::agent_stderr_file(); format!( "failed to connect to rbw-agent \ (this often means that the agent failed to start; \ check {} for agent logs)", log.display() ) }) } fn wait_for_exit(pid: rustix::process::Pid) { loop { if rustix::process::test_kill_process(pid).is_err() { break; } std::thread::sleep(std::time::Duration::from_millis(10)); } } fn get_environment() -> rbw::protocol::Environment { let tty = std::env::var_os("RBW_TTY").or_else(|| { rustix::termios::ttyname(std::io::stdin(), vec![]) .ok() .map(|p| std::ffi::OsString::from_vec(p.as_bytes().to_vec())) }); let env_vars = std::env::vars_os() .filter(|(var_name, _)| { (*rbw::protocol::ENVIRONMENT_VARIABLES_OS).contains(var_name) }) .collect(); rbw::protocol::Environment::new(tty, env_vars) } rbw-1.13.2/src/bin/rbw/commands.rs000064400000000000000000003137631046102023000150170ustar 00000000000000use std::{io::Write as _, os::unix::ffi::OsStrExt as _}; use anyhow::Context as _; const MISSING_CONFIG_HELP: &str = "Before using rbw, you must configure the email address you would like to \ use to log in to the server by running:\n\n \ rbw config set email \n\n\ Additionally, if you are using a self-hosted installation, you should \ run:\n\n \ rbw config set base_url \n\n\ and, if your server has a non-default identity url:\n\n \ rbw config set identity_url \n"; #[derive(Debug, Clone)] pub enum Needle { Name(String), Uri(url::Url), Uuid(uuid::Uuid), } impl std::fmt::Display for Needle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value = match &self { Self::Name(name) => name.clone(), Self::Uri(uri) => uri.to_string(), Self::Uuid(uuid) => uuid.to_string(), }; write!(f, "{value}") } } #[allow(clippy::unnecessary_wraps)] pub fn parse_needle(arg: &str) -> Result { if let Ok(uuid) = uuid::Uuid::parse_str(arg) { return Ok(Needle::Uuid(uuid)); } if let Ok(url) = url::Url::parse(arg) { if url.is_special() { return Ok(Needle::Uri(url)); } } Ok(Needle::Name(arg.to_string())) } #[derive(Debug, Clone, serde::Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedCipher { id: String, folder: Option, name: String, data: DecryptedData, fields: Vec, notes: Option, history: Vec, } impl DecryptedCipher { fn display_short(&self, desc: &str, clipboard: bool) -> bool { match &self.data { DecryptedData::Login { password, .. } => { password.as_ref().map_or_else( || { eprintln!("entry for '{desc}' had no password"); false }, |password| val_display_or_store(clipboard, password), ) } DecryptedData::Card { number, .. } => { number.as_ref().map_or_else( || { eprintln!("entry for '{desc}' had no card number"); false }, |number| val_display_or_store(clipboard, number), ) } DecryptedData::Identity { title, first_name, middle_name, last_name, .. } => { let names: Vec<_> = [title, first_name, middle_name, last_name] .iter() .copied() .flatten() .cloned() .collect(); if names.is_empty() { eprintln!("entry for '{desc}' had no name"); false } else { val_display_or_store(clipboard, &names.join(" ")) } } DecryptedData::SecureNote {} => self.notes.as_ref().map_or_else( || { eprintln!("entry for '{desc}' had no notes"); false }, |notes| val_display_or_store(clipboard, notes), ), } } fn display_field(&self, desc: &str, field: &str, clipboard: bool) { let field = field.to_lowercase(); let field = field.as_str(); match &self.data { DecryptedData::Login { username, totp, uris, .. } => match field { "notes" => { if let Some(notes) = &self.notes { val_display_or_store(clipboard, notes); } } "username" | "user" => { if let Some(username) = &username { val_display_or_store(clipboard, username); } } "totp" | "code" => { if let Some(totp) = totp { match generate_totp(totp) { Ok(code) => { val_display_or_store(clipboard, &code); } Err(e) => { eprintln!("{e}"); } } } } "uris" | "urls" | "sites" => { if let Some(uris) = uris { let uri_strs: Vec<_> = uris .iter() .map(|uri| uri.uri.to_string()) .collect(); val_display_or_store(clipboard, &uri_strs.join("\n")); } } "password" => { self.display_short(desc, clipboard); } _ => { for f in &self.fields { if let Some(name) = &f.name { if name.to_lowercase().as_str().contains(field) { val_display_or_store( clipboard, f.value.as_deref().unwrap_or(""), ); break; } } } } }, DecryptedData::Card { cardholder_name, brand, exp_month, exp_year, code, .. } => match field { "number" | "card" => { self.display_short(desc, clipboard); } "exp" => { if let (Some(month), Some(year)) = (exp_month, exp_year) { val_display_or_store( clipboard, &format!("{month}/{year}"), ); } } "exp_month" | "month" => { if let Some(exp_month) = exp_month { val_display_or_store(clipboard, exp_month); } } "exp_year" | "year" => { if let Some(exp_year) = exp_year { val_display_or_store(clipboard, exp_year); } } "cvv" => { if let Some(code) = code { val_display_or_store(clipboard, code); } } "name" | "cardholder" => { if let Some(cardholder_name) = cardholder_name { val_display_or_store(clipboard, cardholder_name); } } "brand" | "type" => { if let Some(brand) = brand { val_display_or_store(clipboard, brand); } } "notes" => { if let Some(notes) = &self.notes { val_display_or_store(clipboard, notes); } } _ => { for f in &self.fields { if let Some(name) = &f.name { if name.to_lowercase().as_str().contains(field) { val_display_or_store( clipboard, f.value.as_deref().unwrap_or(""), ); break; } } } } }, DecryptedData::Identity { address1, address2, address3, city, state, postal_code, country, phone, email, ssn, license_number, passport_number, username, .. } => match field { "name" => { self.display_short(desc, clipboard); } "email" => { if let Some(email) = email { val_display_or_store(clipboard, email); } } "address" => { let mut strs = vec![]; if let Some(address1) = address1 { strs.push(address1.clone()); } if let Some(address2) = address2 { strs.push(address2.clone()); } if let Some(address3) = address3 { strs.push(address3.clone()); } if !strs.is_empty() { val_display_or_store(clipboard, &strs.join("\n")); } } "city" => { if let Some(city) = city { val_display_or_store(clipboard, city); } } "state" => { if let Some(state) = state { val_display_or_store(clipboard, state); } } "postcode" | "zipcode" | "zip" => { if let Some(postal_code) = postal_code { val_display_or_store(clipboard, postal_code); } } "country" => { if let Some(country) = country { val_display_or_store(clipboard, country); } } "phone" => { if let Some(phone) = phone { val_display_or_store(clipboard, phone); } } "ssn" => { if let Some(ssn) = ssn { val_display_or_store(clipboard, ssn); } } "license" => { if let Some(license_number) = license_number { val_display_or_store(clipboard, license_number); } } "passport" => { if let Some(passport_number) = passport_number { val_display_or_store(clipboard, passport_number); } } "username" => { if let Some(username) = username { val_display_or_store(clipboard, username); } } "notes" => { if let Some(notes) = &self.notes { val_display_or_store(clipboard, notes); } } _ => { for f in &self.fields { if let Some(name) = &f.name { if name.to_lowercase().as_str().contains(field) { val_display_or_store( clipboard, f.value.as_deref().unwrap_or(""), ); break; } } } } }, DecryptedData::SecureNote {} => match field { "note" | "notes" => { self.display_short(desc, clipboard); } _ => { for f in &self.fields { if let Some(name) = &f.name { if name.to_lowercase().as_str().contains(field) { val_display_or_store( clipboard, f.value.as_deref().unwrap_or(""), ); break; } } } } }, } } fn display_long(&self, desc: &str, clipboard: bool) { match &self.data { DecryptedData::Login { username, totp, uris, .. } => { let mut displayed = self.display_short(desc, clipboard); displayed |= display_field("Username", username.as_deref(), clipboard); displayed |= display_field("TOTP Secret", totp.as_deref(), clipboard); if let Some(uris) = uris { for uri in uris { displayed |= display_field("URI", Some(&uri.uri), clipboard); let match_type = uri.match_type.map(|ty| format!("{ty}")); displayed |= display_field( "Match type", match_type.as_deref(), clipboard, ); } } for field in &self.fields { displayed |= display_field( field.name.as_deref().unwrap_or("(null)"), Some(field.value.as_deref().unwrap_or("")), clipboard, ); } if let Some(notes) = &self.notes { if displayed { println!(); } println!("{notes}"); } } DecryptedData::Card { cardholder_name, brand, exp_month, exp_year, code, .. } => { let mut displayed = self.display_short(desc, clipboard); if let (Some(exp_month), Some(exp_year)) = (exp_month, exp_year) { println!("Expiration: {exp_month}/{exp_year}"); displayed = true; } displayed |= display_field("CVV", code.as_deref(), clipboard); displayed |= display_field( "Name", cardholder_name.as_deref(), clipboard, ); displayed |= display_field("Brand", brand.as_deref(), clipboard); if let Some(notes) = &self.notes { if displayed { println!(); } println!("{notes}"); } } DecryptedData::Identity { address1, address2, address3, city, state, postal_code, country, phone, email, ssn, license_number, passport_number, username, .. } => { let mut displayed = self.display_short(desc, clipboard); displayed |= display_field("Address", address1.as_deref(), clipboard); displayed |= display_field("Address", address2.as_deref(), clipboard); displayed |= display_field("Address", address3.as_deref(), clipboard); displayed |= display_field("City", city.as_deref(), clipboard); displayed |= display_field("State", state.as_deref(), clipboard); displayed |= display_field( "Postcode", postal_code.as_deref(), clipboard, ); displayed |= display_field("Country", country.as_deref(), clipboard); displayed |= display_field("Phone", phone.as_deref(), clipboard); displayed |= display_field("Email", email.as_deref(), clipboard); displayed |= display_field("SSN", ssn.as_deref(), clipboard); displayed |= display_field( "License", license_number.as_deref(), clipboard, ); displayed |= display_field( "Passport", passport_number.as_deref(), clipboard, ); displayed |= display_field("Username", username.as_deref(), clipboard); if let Some(notes) = &self.notes { if displayed { println!(); } println!("{notes}"); } } DecryptedData::SecureNote {} => { self.display_short(desc, clipboard); } } } fn display_name(&self) -> String { match &self.data { DecryptedData::Login { username, .. } => { username.as_ref().map_or_else( || self.name.clone(), |username| format!("{}@{}", username, self.name), ) } _ => self.name.clone(), } } fn display_json(&self, desc: &str) -> anyhow::Result<()> { serde_json::to_writer_pretty(std::io::stdout(), &self) .context(format!("failed to write entry '{desc}' to stdout"))?; println!(); Ok(()) } fn exact_match( &self, needle: &Needle, username: Option<&str>, folder: Option<&str>, try_match_folder: bool, ignore_case: bool, ) -> bool { match needle { Needle::Name(name) => { if !((ignore_case && name.to_lowercase() == self.name.to_lowercase()) || *name == self.name) { return false; } } Needle::Uri(given_uri) => { match &self.data { DecryptedData::Login { uris: Some(uris), .. } => { if !uris.iter().any(|uri| uri.matches_url(given_uri)) { return false; } } _ => { // not sure what else to do here, but open to suggestions return false; } } } Needle::Uuid(uuid) => { if uuid::Uuid::parse_str(&self.id) != Ok(*uuid) { return false; } } } if let Some(given_username) = username { match &self.data { DecryptedData::Login { username: Some(found_username), .. } => { if given_username != found_username { return false; } } _ => { // not sure what else to do here, but open to suggestions return false; } } } if try_match_folder { if let Some(given_folder) = folder { if let Some(folder) = &self.folder { if given_folder != folder { return false; } } else { return false; } } else if self.folder.is_some() { return false; } } true } fn partial_match( &self, name: &str, username: Option<&str>, folder: Option<&str>, try_match_folder: bool, ignore_case: bool, ) -> bool { if !((ignore_case && self.name.to_lowercase().contains(&name.to_lowercase())) || self.name.contains(name)) { return false; } if let Some(given_username) = username { match &self.data { DecryptedData::Login { username: Some(found_username), .. } => { if !((ignore_case && found_username .to_lowercase() .contains(&given_username.to_lowercase())) || found_username.contains(given_username)) { return false; } } _ => { // not sure what else to do here, but open to suggestions return false; } } } if try_match_folder { if let Some(given_folder) = folder { if let Some(folder) = &self.folder { if !folder.contains(given_folder) { return false; } } else { return false; } } else if self.folder.is_some() { return false; } } true } fn search_match(&self, term: &str, folder: Option<&str>) -> bool { if let Some(folder) = folder { if self.folder.as_deref() != Some(folder) { return false; } } let fields = [ Some(self.name.as_str()), self.notes.as_deref(), if let DecryptedData::Login { username: Some(username), .. } = &self.data { Some(username) } else { None }, ]; for field in fields .iter() .filter_map(|field| field.map(std::string::ToString::to_string)) .chain(self.fields.iter().filter_map(|field| { field.value.as_ref().map(std::string::ToString::to_string) })) { if field.to_lowercase().contains(&term.to_lowercase()) { return true; } } false } } fn val_display_or_store(clipboard: bool, password: &str) -> bool { if clipboard { match clipboard_store(password) { Ok(()) => true, Err(e) => { eprintln!("{e}"); false } } } else { println!("{password}"); true } } #[derive(Debug, Clone, serde::Serialize)] #[serde(untagged)] #[cfg_attr(test, derive(Eq, PartialEq))] enum DecryptedData { Login { username: Option, password: Option, totp: Option, uris: Option>, }, Card { cardholder_name: Option, number: Option, brand: Option, exp_month: Option, exp_year: Option, code: Option, }, Identity { title: Option, first_name: Option, middle_name: Option, last_name: Option, address1: Option, address2: Option, address3: Option, city: Option, state: Option, postal_code: Option, country: Option, phone: Option, email: Option, ssn: Option, license_number: Option, passport_number: Option, username: Option, }, SecureNote, } #[derive(Debug, Clone, serde::Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedField { name: Option, value: Option, #[serde(serialize_with = "serialize_field_type", rename = "type")] ty: Option, } #[allow(clippy::trivially_copy_pass_by_ref, clippy::ref_option)] fn serialize_field_type( ty: &Option, serializer: S, ) -> Result where S: serde::Serializer, { match ty { Some(ty) => { let s = match ty { rbw::api::FieldType::Text => "text", rbw::api::FieldType::Hidden => "hidden", rbw::api::FieldType::Boolean => "boolean", rbw::api::FieldType::Linked => "linked", }; serializer.serialize_some(&Some(s)) } None => serializer.serialize_none(), } } #[derive(Debug, Clone, serde::Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedHistoryEntry { last_used_date: String, password: String, } #[derive(Debug, Clone, serde::Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedUri { uri: String, match_type: Option, } impl DecryptedUri { fn matches_url(&self, url: &url::Url) -> bool { match self.match_type.unwrap_or(rbw::api::UriMatchType::Domain) { rbw::api::UriMatchType::Domain => { let Some(given_host_port) = host_port(url) else { return false; }; if let Ok(self_url) = url::Url::parse(&self.uri) { if let Some(self_host_port) = host_port(&self_url) { if self_url.scheme() == url.scheme() && (self_host_port == given_host_port || given_host_port .ends_with(&format!(".{self_host_port}"))) { return true; } } } self.uri == given_host_port || given_host_port.ends_with(&format!(".{}", self.uri)) } rbw::api::UriMatchType::Host => { let Some(given_host_port) = host_port(url) else { return false; }; if let Ok(self_url) = url::Url::parse(&self.uri) { if let Some(self_host_port) = host_port(&self_url) { if self_url.scheme() == url.scheme() && self_host_port == given_host_port { return true; } } } self.uri == given_host_port } rbw::api::UriMatchType::StartsWith => { url.to_string().starts_with(&self.uri) } rbw::api::UriMatchType::Exact => { if url.path() == "/" { url.to_string().trim_end_matches('/') == self.uri.trim_end_matches('/') } else { url.to_string() == self.uri } } rbw::api::UriMatchType::RegularExpression => { let Ok(rx) = regex::Regex::new(&self.uri) else { return false; }; rx.is_match(url.as_ref()) } rbw::api::UriMatchType::Never => false, } } } fn host_port(url: &url::Url) -> Option { let host = url.host_str()?; Some( url.port().map_or_else( || host.to_string(), |port| format!("{host}:{port}"), ), ) } enum ListField { Name, Id, User, Folder, } impl std::convert::TryFrom<&String> for ListField { type Error = anyhow::Error; fn try_from(s: &String) -> anyhow::Result { Ok(match s.as_str() { "name" => Self::Name, "id" => Self::Id, "user" => Self::User, "folder" => Self::Folder, _ => return Err(anyhow::anyhow!("unknown field {}", s)), }) } } const HELP_PW: &str = r" # The first line of this file will be the password, and the remainder of the # file (after any blank lines after the password) will be stored as a note. # Lines with leading # will be ignored. "; const HELP_NOTES: &str = r" # The content of this file will be stored as a note. # Lines with leading # will be ignored. "; pub fn config_show() -> anyhow::Result<()> { let config = rbw::config::Config::load()?; serde_json::to_writer_pretty(std::io::stdout(), &config) .context("failed to write config to stdout")?; println!(); Ok(()) } pub fn config_set(key: &str, value: &str) -> anyhow::Result<()> { let mut config = rbw::config::Config::load() .unwrap_or_else(|_| rbw::config::Config::new()); match key { "email" => config.email = Some(value.to_string()), "sso_id" => config.sso_id = Some(value.to_string()), "base_url" => config.base_url = Some(value.to_string()), "identity_url" => config.identity_url = Some(value.to_string()), "ui_url" => config.ui_url = Some(value.to_string()), "notifications_url" => { config.notifications_url = Some(value.to_string()); } "client_cert_path" => { config.client_cert_path = Some(std::path::PathBuf::from(value.to_string())); } "lock_timeout" => { let timeout = value .parse() .context("failed to parse value for lock_timeout")?; if timeout == 0 { log::error!("lock_timeout must be greater than 0"); } else { config.lock_timeout = timeout; } } "sync_interval" => { let interval = value .parse() .context("failed to parse value for sync_interval")?; config.sync_interval = interval; } "pinentry" => config.pinentry = value.to_string(), _ => return Err(anyhow::anyhow!("invalid config key: {}", key)), } config.save()?; // drop in-memory keys, since they will be different if the email or url // changed. not using lock() because we don't want to require the agent to // be running (since this may be the user running `rbw config set // base_url` as the first operation), and stop_agent() already handles the // agent not running case gracefully. stop_agent()?; Ok(()) } pub fn config_unset(key: &str) -> anyhow::Result<()> { let mut config = rbw::config::Config::load() .unwrap_or_else(|_| rbw::config::Config::new()); match key { "email" => config.email = None, "sso_id" => config.sso_id = None, "base_url" => config.base_url = None, "identity_url" => config.identity_url = None, "ui_url" => config.ui_url = None, "notifications_url" => config.notifications_url = None, "client_cert_path" => config.client_cert_path = None, "lock_timeout" => { config.lock_timeout = rbw::config::default_lock_timeout(); } "pinentry" => config.pinentry = rbw::config::default_pinentry(), _ => return Err(anyhow::anyhow!("invalid config key: {}", key)), } config.save()?; // drop in-memory keys, since they will be different if the email or url // changed. not using lock() because we don't want to require the agent to // be running (since this may be the user running `rbw config set // base_url` as the first operation), and stop_agent() already handles the // agent not running case gracefully. stop_agent()?; Ok(()) } fn clipboard_store(val: &str) -> anyhow::Result<()> { ensure_agent()?; crate::actions::clipboard_store(val)?; Ok(()) } pub fn register() -> anyhow::Result<()> { ensure_agent()?; crate::actions::register()?; Ok(()) } pub fn login() -> anyhow::Result<()> { ensure_agent()?; crate::actions::login()?; Ok(()) } pub fn unlock() -> anyhow::Result<()> { ensure_agent()?; crate::actions::login()?; crate::actions::unlock()?; Ok(()) } pub fn unlocked() -> anyhow::Result<()> { // ensure_agent()?; crate::actions::unlocked()?; Ok(()) } pub fn sync() -> anyhow::Result<()> { ensure_agent()?; crate::actions::login()?; crate::actions::sync()?; Ok(()) } pub fn list(fields: &[String]) -> anyhow::Result<()> { let fields: Vec = fields .iter() .map(std::convert::TryFrom::try_from) .collect::>()?; unlock()?; let db = load_db()?; let mut ciphers: Vec = db .entries .iter() .map(decrypt_cipher) .collect::>()?; ciphers.sort_unstable_by(|a, b| a.name.cmp(&b.name)); for cipher in ciphers { let values: Vec = fields .iter() .map(|field| match field { ListField::Name => cipher.name.clone(), ListField::Id => cipher.id.clone(), ListField::User => match &cipher.data { DecryptedData::Login { username, .. } => { username.as_ref().map_or_else( String::new, std::string::ToString::to_string, ) } _ => String::new(), }, ListField::Folder => cipher.folder.as_ref().map_or_else( String::new, std::string::ToString::to_string, ), }) .collect(); // write to stdout but don't panic when pipe get's closed // this happens when piping stdout in a shell match writeln!(&mut std::io::stdout(), "{}", values.join("\t")) { Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()), res => res, }?; } Ok(()) } #[allow(clippy::fn_params_excessive_bools)] pub fn get( needle: &Needle, user: Option<&str>, folder: Option<&str>, field: Option<&str>, full: bool, raw: bool, clipboard: bool, ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; let db = load_db()?; let desc = format!( "{}{}", user.map_or_else(String::new, |s| format!("{s}@")), needle ); let (_, decrypted) = find_entry(&db, needle, user, folder, ignore_case) .with_context(|| format!("couldn't find entry for '{desc}'"))?; if raw { decrypted.display_json(&desc)?; } else if full { decrypted.display_long(&desc, clipboard); } else if let Some(field) = field { decrypted.display_field(&desc, field, clipboard); } else { decrypted.display_short(&desc, clipboard); } Ok(()) } pub fn search(term: &str, folder: Option<&str>) -> anyhow::Result<()> { unlock()?; let db = load_db()?; let found_entries: Vec<_> = db .entries .iter() .map(decrypt_cipher) .filter_map(|entry| { entry .map(|decrypted| { if decrypted.search_match(term, folder) { let mut display = decrypted.name; if let DecryptedData::Login { username: Some(username), .. } = decrypted.data { display = format!("{username}@{display}"); } if let Some(folder) = decrypted.folder { display = format!("{folder}/{display}"); } Some(display) } else { None } }) .transpose() }) .collect::>()?; for name in found_entries { println!("{name}"); } Ok(()) } pub fn code( needle: &Needle, user: Option<&str>, folder: Option<&str>, clipboard: bool, ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; let db = load_db()?; let desc = format!( "{}{}", user.map_or_else(String::new, |s| format!("{s}@")), needle ); let (_, decrypted) = find_entry(&db, needle, user, folder, ignore_case) .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let DecryptedData::Login { totp, .. } = decrypted.data { if let Some(totp) = totp { val_display_or_store(clipboard, &generate_totp(&totp)?); } else { return Err(anyhow::anyhow!( "entry does not contain a totp secret" )); } } else { return Err(anyhow::anyhow!("not a login entry")); } Ok(()) } pub fn add( name: &str, username: Option<&str>, uris: &[(String, Option)], folder: Option<&str>, ) -> anyhow::Result<()> { unlock()?; let mut db = load_db()?; // unwrap is safe here because the call to unlock above is guaranteed to // populate these or error let mut access_token = db.access_token.as_ref().unwrap().clone(); let refresh_token = db.refresh_token.as_ref().unwrap(); let name = crate::actions::encrypt(name, None)?; let username = username .map(|username| crate::actions::encrypt(username, None)) .transpose()?; let contents = rbw::edit::edit("", HELP_PW)?; let (password, notes) = parse_editor(&contents); let password = password .map(|password| crate::actions::encrypt(&password, None)) .transpose()?; let notes = notes .map(|notes| crate::actions::encrypt(¬es, None)) .transpose()?; let uris: Vec<_> = uris .iter() .map(|uri| { Ok(rbw::db::Uri { uri: crate::actions::encrypt(&uri.0, None)?, match_type: uri.1, }) }) .collect::>()?; let mut folder_id = None; if let Some(folder_name) = folder { let (new_access_token, folders) = rbw::actions::list_folders(&access_token, refresh_token)?; if let Some(new_access_token) = new_access_token { access_token.clone_from(&new_access_token); db.access_token = Some(new_access_token); save_db(&db)?; } let folders: Vec<(String, String)> = folders .iter() .cloned() .map(|(id, name)| { Ok((id, crate::actions::decrypt(&name, None, None)?)) }) .collect::>()?; for (id, name) in folders { if name == folder_name { folder_id = Some(id); } } if folder_id.is_none() { let (new_access_token, id) = rbw::actions::create_folder( &access_token, refresh_token, &crate::actions::encrypt(folder_name, None)?, )?; if let Some(new_access_token) = new_access_token { access_token.clone_from(&new_access_token); db.access_token = Some(new_access_token); save_db(&db)?; } folder_id = Some(id); } } if let (Some(access_token), ()) = rbw::actions::add( &access_token, refresh_token, &name, &rbw::db::EntryData::Login { username, password, uris, totp: None, }, notes.as_deref(), folder_id.as_deref(), )? { db.access_token = Some(access_token); save_db(&db)?; } crate::actions::sync()?; Ok(()) } pub fn generate( name: Option<&str>, username: Option<&str>, uris: &[(String, Option)], folder: Option<&str>, len: usize, ty: rbw::pwgen::Type, ) -> anyhow::Result<()> { let password = rbw::pwgen::pwgen(ty, len); println!("{password}"); if let Some(name) = name { unlock()?; let mut db = load_db()?; // unwrap is safe here because the call to unlock above is guaranteed // to populate these or error let mut access_token = db.access_token.as_ref().unwrap().clone(); let refresh_token = db.refresh_token.as_ref().unwrap(); let name = crate::actions::encrypt(name, None)?; let username = username .map(|username| crate::actions::encrypt(username, None)) .transpose()?; let password = crate::actions::encrypt(&password, None)?; let uris: Vec<_> = uris .iter() .map(|uri| { Ok(rbw::db::Uri { uri: crate::actions::encrypt(&uri.0, None)?, match_type: uri.1, }) }) .collect::>()?; let mut folder_id = None; if let Some(folder_name) = folder { let (new_access_token, folders) = rbw::actions::list_folders(&access_token, refresh_token)?; if let Some(new_access_token) = new_access_token { access_token.clone_from(&new_access_token); db.access_token = Some(new_access_token); save_db(&db)?; } let folders: Vec<(String, String)> = folders .iter() .cloned() .map(|(id, name)| { Ok((id, crate::actions::decrypt(&name, None, None)?)) }) .collect::>()?; for (id, name) in folders { if name == folder_name { folder_id = Some(id); } } if folder_id.is_none() { let (new_access_token, id) = rbw::actions::create_folder( &access_token, refresh_token, &crate::actions::encrypt(folder_name, None)?, )?; if let Some(new_access_token) = new_access_token { access_token.clone_from(&new_access_token); db.access_token = Some(new_access_token); save_db(&db)?; } folder_id = Some(id); } } if let (Some(access_token), ()) = rbw::actions::add( &access_token, refresh_token, &name, &rbw::db::EntryData::Login { username, password: Some(password), uris, totp: None, }, None, folder_id.as_deref(), )? { db.access_token = Some(access_token); save_db(&db)?; } crate::actions::sync()?; } Ok(()) } pub fn edit( name: &str, username: Option<&str>, folder: Option<&str>, ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; let mut db = load_db()?; let access_token = db.access_token.as_ref().unwrap(); let refresh_token = db.refresh_token.as_ref().unwrap(); let desc = format!( "{}{}", username.map_or_else(String::new, |s| format!("{s}@")), name ); let (entry, decrypted) = find_entry( &db, &Needle::Name(name.to_string()), username, folder, ignore_case, ) .with_context(|| format!("couldn't find entry for '{desc}'"))?; let (data, fields, notes, history) = match &decrypted.data { DecryptedData::Login { password, .. } => { let mut contents = format!("{}\n", password.as_deref().unwrap_or("")); if let Some(notes) = decrypted.notes { contents.push_str(&format!("\n{notes}\n")); } let contents = rbw::edit::edit(&contents, HELP_PW)?; let (password, notes) = parse_editor(&contents); let password = password .map(|password| { crate::actions::encrypt( &password, entry.org_id.as_deref(), ) }) .transpose()?; let notes = notes .map(|notes| { crate::actions::encrypt(¬es, entry.org_id.as_deref()) }) .transpose()?; let mut history = entry.history.clone(); let rbw::db::EntryData::Login { username: entry_username, password: entry_password, uris: entry_uris, totp: entry_totp, } = &entry.data else { unreachable!(); }; if let Some(prev_password) = entry_password.clone() { let new_history_entry = rbw::db::HistoryEntry { last_used_date: format!( "{}", humantime::format_rfc3339( std::time::SystemTime::now() ) ), password: prev_password, }; history.insert(0, new_history_entry); } let data = rbw::db::EntryData::Login { username: entry_username.clone(), password, uris: entry_uris.clone(), totp: entry_totp.clone(), }; (data, entry.fields, notes, history) } DecryptedData::SecureNote {} => { let data = rbw::db::EntryData::SecureNote {}; let editor_content = decrypted.notes.map_or_else( || "\n".to_string(), |notes| format!("{notes}\n"), ); let contents = rbw::edit::edit(&editor_content, HELP_NOTES)?; // prepend blank line to be parsed as pw by `parse_editor` let (_, notes) = parse_editor(&format!("\n{contents}\n")); let notes = notes .map(|notes| { crate::actions::encrypt(¬es, entry.org_id.as_deref()) }) .transpose()?; (data, entry.fields, notes, entry.history) } _ => { return Err(anyhow::anyhow!( "modifications are only supported for login and note entries" )); } }; if let (Some(access_token), ()) = rbw::actions::edit( access_token, refresh_token, &entry.id, entry.org_id.as_deref(), &entry.name, &data, &fields, notes.as_deref(), entry.folder_id.as_deref(), &history, )? { db.access_token = Some(access_token); save_db(&db)?; } crate::actions::sync()?; Ok(()) } pub fn remove( name: &str, username: Option<&str>, folder: Option<&str>, ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; let mut db = load_db()?; let access_token = db.access_token.as_ref().unwrap(); let refresh_token = db.refresh_token.as_ref().unwrap(); let desc = format!( "{}{}", username.map_or_else(String::new, |s| format!("{s}@")), name ); let (entry, _) = find_entry( &db, &Needle::Name(name.to_string()), username, folder, ignore_case, ) .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let (Some(access_token), ()) = rbw::actions::remove(access_token, refresh_token, &entry.id)? { db.access_token = Some(access_token); save_db(&db)?; } crate::actions::sync()?; Ok(()) } pub fn history( name: &str, username: Option<&str>, folder: Option<&str>, ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; let db = load_db()?; let desc = format!( "{}{}", username.map_or_else(String::new, |s| format!("{s}@")), name ); let (_, decrypted) = find_entry( &db, &Needle::Name(name.to_string()), username, folder, ignore_case, ) .with_context(|| format!("couldn't find entry for '{desc}'"))?; for history in decrypted.history { println!("{}: {}", history.last_used_date, history.password); } Ok(()) } pub fn lock() -> anyhow::Result<()> { ensure_agent()?; crate::actions::lock()?; Ok(()) } pub fn purge() -> anyhow::Result<()> { stop_agent()?; remove_db()?; Ok(()) } pub fn stop_agent() -> anyhow::Result<()> { crate::actions::quit()?; Ok(()) } fn ensure_agent() -> anyhow::Result<()> { check_config()?; ensure_agent_once()?; let client_version = rbw::protocol::version(); let agent_version = version_or_quit()?; if agent_version != client_version { log::debug!( "client protocol version is {} but agent protocol version is {}", client_version, agent_version ); crate::actions::quit()?; ensure_agent_once()?; let agent_version = version_or_quit()?; if agent_version != client_version { crate::actions::quit()?; return Err(anyhow::anyhow!( "incompatible protocol versions: client ({}), agent ({})", client_version, agent_version )); } } Ok(()) } fn ensure_agent_once() -> anyhow::Result<()> { let agent_path = std::env::var_os("RBW_AGENT"); let agent_path = agent_path .as_deref() .unwrap_or_else(|| std::ffi::OsStr::from_bytes(b"rbw-agent")); let status = std::process::Command::new(agent_path) .status() .context("failed to run rbw-agent")?; if !status.success() { if let Some(code) = status.code() { if code != 23 { return Err(anyhow::anyhow!( "failed to run rbw-agent: {}", status )); } } } Ok(()) } fn check_config() -> anyhow::Result<()> { rbw::config::Config::validate().map_err(|e| { log::error!("{}", MISSING_CONFIG_HELP); anyhow::Error::new(e) }) } fn version_or_quit() -> anyhow::Result { crate::actions::version().inspect_err(|_| { let _ = crate::actions::quit(); }) } fn find_entry( db: &rbw::db::Db, needle: &Needle, username: Option<&str>, folder: Option<&str>, ignore_case: bool, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { if let Needle::Uuid(uuid) = needle { for cipher in &db.entries { if uuid::Uuid::parse_str(&cipher.id) == Ok(*uuid) { return Ok((cipher.clone(), decrypt_cipher(cipher)?)); } } Err(anyhow::anyhow!("no entry found")) } else { let ciphers: Vec<(rbw::db::Entry, DecryptedCipher)> = db .entries .iter() .map(|entry| { decrypt_cipher(entry) .map(|decrypted| (entry.clone(), decrypted)) }) .collect::>()?; find_entry_raw(&ciphers, needle, username, folder, ignore_case) } } fn find_entry_raw( entries: &[(rbw::db::Entry, DecryptedCipher)], needle: &Needle, username: Option<&str>, folder: Option<&str>, ignore_case: bool, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { let mut matches: Vec<(rbw::db::Entry, DecryptedCipher)> = entries .iter() .filter(|&(_, decrypted_cipher)| { decrypted_cipher.exact_match( needle, username, folder, true, ignore_case, ) }) .cloned() .collect(); if matches.len() == 1 { return Ok(matches[0].clone()); } if folder.is_none() { matches = entries .iter() .filter(|&(_, decrypted_cipher)| { decrypted_cipher.exact_match( needle, username, folder, false, ignore_case, ) }) .cloned() .collect(); if matches.len() == 1 { return Ok(matches[0].clone()); } } if let Needle::Name(name) = needle { matches = entries .iter() .filter(|&(_, decrypted_cipher)| { decrypted_cipher.partial_match( name, username, folder, true, ignore_case, ) }) .cloned() .collect(); if matches.len() == 1 { return Ok(matches[0].clone()); } if folder.is_none() { matches = entries .iter() .filter(|&(_, decrypted_cipher)| { decrypted_cipher.partial_match( name, username, folder, false, ignore_case, ) }) .cloned() .collect(); if matches.len() == 1 { return Ok(matches[0].clone()); } } } if matches.is_empty() { Err(anyhow::anyhow!("no entry found")) } else { let entries: Vec = matches .iter() .map(|(_, decrypted)| decrypted.display_name()) .collect(); let entries = entries.join(", "); Err(anyhow::anyhow!("multiple entries found: {}", entries)) } } fn decrypt_field( name: &str, field: Option<&str>, entry_key: Option<&str>, org_id: Option<&str>, ) -> Option { let field = field .as_ref() .map(|field| crate::actions::decrypt(field, entry_key, org_id)) .transpose(); match field { Ok(field) => field, Err(e) => { log::warn!("failed to decrypt {}: {}", name, e); None } } } fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result { // folder name should always be decrypted with the local key because // folders are local to a specific user's vault, not the organization let folder = entry .folder .as_ref() .map(|folder| crate::actions::decrypt(folder, None, None)) .transpose(); let folder = match folder { Ok(folder) => folder, Err(e) => { log::warn!("failed to decrypt folder name: {}", e); None } }; let fields = entry .fields .iter() .map(|field| { Ok(DecryptedField { name: field .name .as_ref() .map(|name| { crate::actions::decrypt( name, entry.key.as_deref(), entry.org_id.as_deref(), ) }) .transpose()?, value: field .value .as_ref() .map(|value| { crate::actions::decrypt( value, entry.key.as_deref(), entry.org_id.as_deref(), ) }) .transpose()?, ty: field.ty, }) }) .collect::>()?; let notes = entry .notes .as_ref() .map(|notes| { crate::actions::decrypt( notes, entry.key.as_deref(), entry.org_id.as_deref(), ) }) .transpose(); let notes = match notes { Ok(notes) => notes, Err(e) => { log::warn!("failed to decrypt notes: {}", e); None } }; let history = entry .history .iter() .map(|history_entry| { Ok(DecryptedHistoryEntry { last_used_date: history_entry.last_used_date.clone(), password: crate::actions::decrypt( &history_entry.password, entry.key.as_deref(), entry.org_id.as_deref(), )?, }) }) .collect::>()?; let data = match &entry.data { rbw::db::EntryData::Login { username, password, totp, uris, } => DecryptedData::Login { username: decrypt_field( "username", username.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), password: decrypt_field( "password", password.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), totp: decrypt_field( "totp", totp.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), uris: uris .iter() .map(|s| { decrypt_field( "uri", Some(&s.uri), entry.key.as_deref(), entry.org_id.as_deref(), ) .map(|uri| DecryptedUri { uri, match_type: s.match_type, }) }) .collect(), }, rbw::db::EntryData::Card { cardholder_name, number, brand, exp_month, exp_year, code, } => DecryptedData::Card { cardholder_name: decrypt_field( "cardholder_name", cardholder_name.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), number: decrypt_field( "number", number.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), brand: decrypt_field( "brand", brand.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), exp_month: decrypt_field( "exp_month", exp_month.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), exp_year: decrypt_field( "exp_year", exp_year.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), code: decrypt_field( "code", code.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), }, rbw::db::EntryData::Identity { title, first_name, middle_name, last_name, address1, address2, address3, city, state, postal_code, country, phone, email, ssn, license_number, passport_number, username, } => DecryptedData::Identity { title: decrypt_field( "title", title.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), first_name: decrypt_field( "first_name", first_name.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), middle_name: decrypt_field( "middle_name", middle_name.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), last_name: decrypt_field( "last_name", last_name.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), address1: decrypt_field( "address1", address1.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), address2: decrypt_field( "address2", address2.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), address3: decrypt_field( "address3", address3.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), city: decrypt_field( "city", city.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), state: decrypt_field( "state", state.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), postal_code: decrypt_field( "postal_code", postal_code.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), country: decrypt_field( "country", country.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), phone: decrypt_field( "phone", phone.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), email: decrypt_field( "email", email.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), ssn: decrypt_field( "ssn", ssn.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), license_number: decrypt_field( "license_number", license_number.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), passport_number: decrypt_field( "passport_number", passport_number.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), username: decrypt_field( "username", username.as_deref(), entry.key.as_deref(), entry.org_id.as_deref(), ), }, rbw::db::EntryData::SecureNote {} => DecryptedData::SecureNote {}, }; Ok(DecryptedCipher { id: entry.id.clone(), folder, name: crate::actions::decrypt( &entry.name, entry.key.as_deref(), entry.org_id.as_deref(), )?, data, fields, notes, history, }) } fn parse_editor(contents: &str) -> (Option, Option) { let mut lines = contents.lines(); let password = lines.next().map(std::string::ToString::to_string); let mut notes: String = lines .skip_while(|line| line.is_empty()) .filter(|line| !line.starts_with('#')) .fold(String::new(), |mut notes, line| { notes.push_str(line); notes.push('\n'); notes }); while notes.ends_with('\n') { notes.pop(); } let notes = if notes.is_empty() { None } else { Some(notes) }; (password, notes) } fn load_db() -> anyhow::Result { let config = rbw::config::Config::load()?; config.email.as_ref().map_or_else( || Err(anyhow::anyhow!("failed to find email address in config")), |email| { rbw::db::Db::load(&config.server_name(), email) .map_err(anyhow::Error::new) }, ) } fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> { let config = rbw::config::Config::load()?; config.email.as_ref().map_or_else( || Err(anyhow::anyhow!("failed to find email address in config")), |email| { db.save(&config.server_name(), email) .map_err(anyhow::Error::new) }, ) } fn remove_db() -> anyhow::Result<()> { let config = rbw::config::Config::load()?; config.email.as_ref().map_or_else( || Err(anyhow::anyhow!("failed to find email address in config")), |email| { rbw::db::Db::remove(&config.server_name(), email) .map_err(anyhow::Error::new) }, ) } struct TotpParams { secret: Vec, algorithm: String, digits: u32, period: u64, } fn decode_totp_secret(secret: &str) -> anyhow::Result> { let secret = secret.trim(); let alphabets = [ base32::Alphabet::Rfc4648 { padding: false }, base32::Alphabet::Rfc4648 { padding: true }, base32::Alphabet::Rfc4648Lower { padding: false }, base32::Alphabet::Rfc4648Lower { padding: true }, ]; for alphabet in alphabets { if let Some(secret) = base32::decode(alphabet, secret) { return Ok(secret); } } Err(anyhow::anyhow!("totp secret was not valid base32")) } fn parse_totp_secret(secret: &str) -> anyhow::Result { if let Ok(u) = url::Url::parse(secret) { if u.scheme() != "otpauth" { return Err(anyhow::anyhow!( "totp secret url must have otpauth scheme" )); } if u.host_str() != Some("totp") { return Err(anyhow::anyhow!( "totp secret url must have totp host" )); } let query: std::collections::HashMap<_, _> = u.query_pairs().collect(); Ok(TotpParams { secret: decode_totp_secret(query .get("secret") .ok_or_else(|| { anyhow::anyhow!("totp secret url must have secret") })?)?, algorithm:query.get("algorithm").map_or_else(||{String::from("SHA1")},|alg|{alg.to_string()} ), digits: match query.get("digits") { Some(dig) => { dig.parse::().map_err(|_|{ anyhow::anyhow!("digits parameter in totp url must be a valid integer.") })? } None => 6, }, period: match query.get("period") { Some(dig) => { dig.parse::().map_err(|_|{ anyhow::anyhow!("period parameter in totp url must be a valid integer.") })? } None => totp_lite::DEFAULT_STEP, } }) } else { Ok(TotpParams { secret: decode_totp_secret(secret)?, algorithm: String::from("SHA1"), digits: 6, period: totp_lite::DEFAULT_STEP, }) } } fn generate_totp(secret: &str) -> anyhow::Result { let totp_params = parse_totp_secret(secret)?; let alg = totp_params.algorithm.as_str(); match alg { "SHA1" => Ok(totp_lite::totp_custom::( totp_params.period, totp_params.digits, &totp_params.secret, std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH)? .as_secs(), )), "SHA256" => Ok(totp_lite::totp_custom::( totp_params.period, totp_params.digits, &totp_params.secret, std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH)? .as_secs(), )), "SHA512" => Ok(totp_lite::totp_custom::( totp_params.period, totp_params.digits, &totp_params.secret, std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH)? .as_secs(), )), _ => Err(anyhow::anyhow!(format!( "{} is not a valid totp algorithm", alg ))), } } fn display_field(name: &str, field: Option<&str>, clipboard: bool) -> bool { field.map_or_else( || false, |field| val_display_or_store(clipboard, &format!("{name}: {field}")), ) } #[cfg(test)] mod test { use super::*; #[test] fn test_find_entry() { let entries = &[ make_entry("github", Some("foo"), None, &[]), make_entry("gitlab", Some("foo"), None, &[]), make_entry("gitlab", Some("bar"), None, &[]), make_entry("gitter", Some("baz"), None, &[]), make_entry("git", Some("foo"), None, &[]), make_entry("bitwarden", None, None, &[]), make_entry("github", Some("foo"), Some("websites"), &[]), make_entry("github", Some("foo"), Some("ssh"), &[]), make_entry("github", Some("root"), Some("ssh"), &[]), ]; assert!( one_match(entries, "github", Some("foo"), None, 0, false), "foo@github" ); assert!( one_match(entries, "GITHUB", Some("foo"), None, 0, true), "foo@GITHUB" ); assert!(one_match(entries, "github", None, None, 0, false), "github"); assert!(one_match(entries, "GITHUB", None, None, 0, true), "GITHUB"); assert!( one_match(entries, "gitlab", Some("foo"), None, 1, false), "foo@gitlab" ); assert!( one_match(entries, "GITLAB", Some("foo"), None, 1, true), "foo@GITLAB" ); assert!( one_match(entries, "git", Some("bar"), None, 2, false), "bar@git" ); assert!( one_match(entries, "GIT", Some("bar"), None, 2, true), "bar@GIT" ); assert!( one_match(entries, "gitter", Some("ba"), None, 3, false), "ba@gitter" ); assert!( one_match(entries, "GITTER", Some("ba"), None, 3, true), "ba@GITTER" ); assert!( one_match(entries, "git", Some("foo"), None, 4, false), "foo@git" ); assert!( one_match(entries, "GIT", Some("foo"), None, 4, true), "foo@GIT" ); assert!(one_match(entries, "git", None, None, 4, false), "git"); assert!(one_match(entries, "GIT", None, None, 4, true), "GIT"); assert!( one_match(entries, "bitwarden", None, None, 5, false), "bitwarden" ); assert!( one_match(entries, "BITWARDEN", None, None, 5, true), "BITWARDEN" ); assert!( one_match( entries, "github", Some("foo"), Some("websites"), 6, false ), "websites/foo@github" ); assert!( one_match( entries, "GITHUB", Some("foo"), Some("websites"), 6, true ), "websites/foo@GITHUB" ); assert!( one_match(entries, "github", Some("foo"), Some("ssh"), 7, false), "ssh/foo@github" ); assert!( one_match(entries, "GITHUB", Some("foo"), Some("ssh"), 7, true), "ssh/foo@GITHUB" ); assert!( one_match(entries, "github", Some("root"), None, 8, false), "ssh/root@github" ); assert!( one_match(entries, "GITHUB", Some("root"), None, 8, true), "ssh/root@GITHUB" ); assert!( no_matches(entries, "gitlab", Some("baz"), None, false), "baz@gitlab" ); assert!( no_matches(entries, "GITLAB", Some("baz"), None, true), "baz@" ); assert!( no_matches(entries, "bitbucket", Some("foo"), None, false), "foo@bitbucket" ); assert!( no_matches(entries, "BITBUCKET", Some("foo"), None, true), "foo@BITBUCKET" ); assert!( no_matches(entries, "github", Some("foo"), Some("bar"), false), "bar/foo@github" ); assert!( no_matches(entries, "GITHUB", Some("foo"), Some("bar"), true), "bar/foo@" ); assert!( no_matches(entries, "gitlab", Some("foo"), Some("bar"), false), "bar/foo@gitlab" ); assert!( no_matches(entries, "GITLAB", Some("foo"), Some("bar"), true), "bar/foo@GITLAB" ); assert!(many_matches(entries, "gitlab", None, None, false), "gitlab"); assert!(many_matches(entries, "gitlab", None, None, true), "GITLAB"); assert!( many_matches(entries, "gi", Some("foo"), None, false), "foo@gi" ); assert!( many_matches(entries, "GI", Some("foo"), None, true), "foo@GI" ); assert!( many_matches(entries, "git", Some("ba"), None, false), "ba@git" ); assert!( many_matches(entries, "GIT", Some("ba"), None, true), "ba@GIT" ); assert!( many_matches(entries, "github", Some("foo"), Some("s"), false), "s/foo@github" ); assert!( many_matches(entries, "GITHUB", Some("foo"), Some("s"), true), "s/foo@GITHUB" ); } #[test] fn test_find_by_uuid() { let entries = &[ make_entry("github", Some("foo"), None, &[]), make_entry("gitlab", Some("foo"), None, &[]), make_entry("gitlab", Some("bar"), None, &[]), ]; assert!( one_match(entries, &entries[0].0.id, None, None, 0, false), "foo@github" ); assert!( one_match(entries, &entries[1].0.id, None, None, 1, false), "foo@gitlab" ); assert!( one_match(entries, &entries[2].0.id, None, None, 2, false), "bar@gitlab" ); assert!( one_match( entries, &entries[0].0.id.to_uppercase(), None, None, 0, false ), "foo@github" ); assert!( one_match( entries, &entries[0].0.id.to_lowercase(), None, None, 0, false ), "foo@github" ); } #[test] fn test_find_by_url_default() { let entries = &[ make_entry("one", None, None, &[("https://one.com/", None)]), make_entry("two", None, None, &[("https://two.com/login", None)]), make_entry( "three", None, None, &[("https://login.three.com/", None)], ), make_entry("four", None, None, &[("four.com", None)]), make_entry( "five", None, None, &[("https://five.com:8080/", None)], ), make_entry("six", None, None, &[("six.com:8080", None)]), make_entry("seven", None, None, &[("192.168.0.128:8080", None)]), ]; assert!( one_match(entries, "https://one.com/", None, None, 0, false), "one" ); assert!( one_match( entries, "https://login.one.com/", None, None, 0, false ), "one" ); assert!( one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); assert!(no_matches(entries, "one.com", None, None, false), "one"); assert!(no_matches(entries, "https", None, None, false), "one"); assert!(no_matches(entries, "com", None, None, false), "one"); assert!( no_matches(entries, "https://com/", None, None, false), "one" ); assert!( one_match(entries, "https://two.com/", None, None, 1, false), "two" ); assert!( one_match( entries, "https://two.com/other-page", None, None, 1, false ), "two" ); assert!( one_match( entries, "https://login.three.com/", None, None, 2, false ), "three" ); assert!( no_matches(entries, "https://three.com/", None, None, false), "three" ); assert!( one_match(entries, "https://four.com/", None, None, 3, false), "four" ); assert!( one_match( entries, "https://five.com:8080/", None, None, 4, false ), "five" ); assert!( no_matches(entries, "https://five.com/", None, None, false), "five" ); assert!( one_match(entries, "https://six.com:8080/", None, None, 5, false), "six" ); assert!( no_matches(entries, "https://six.com/", None, None, false), "six" ); assert!( one_match( entries, "https://192.168.0.128:8080/", None, None, 6, false ), "seven" ); assert!( no_matches(entries, "https://192.168.0.128/", None, None, false), "seven" ); } #[test] fn test_find_by_url_domain() { let entries = &[ make_entry( "one", None, None, &[("https://one.com/", Some(rbw::api::UriMatchType::Domain))], ), make_entry( "two", None, None, &[( "https://two.com/login", Some(rbw::api::UriMatchType::Domain), )], ), make_entry( "three", None, None, &[( "https://login.three.com/", Some(rbw::api::UriMatchType::Domain), )], ), make_entry( "four", None, None, &[("four.com", Some(rbw::api::UriMatchType::Domain))], ), make_entry( "five", None, None, &[( "https://five.com:8080/", Some(rbw::api::UriMatchType::Domain), )], ), make_entry( "six", None, None, &[("six.com:8080", Some(rbw::api::UriMatchType::Domain))], ), make_entry( "seven", None, None, &[( "192.168.0.128:8080", Some(rbw::api::UriMatchType::Domain), )], ), ]; assert!( one_match(entries, "https://one.com/", None, None, 0, false), "one" ); assert!( one_match( entries, "https://login.one.com/", None, None, 0, false ), "one" ); assert!( one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); assert!(no_matches(entries, "one.com", None, None, false), "one"); assert!(no_matches(entries, "https", None, None, false), "one"); assert!(no_matches(entries, "com", None, None, false), "one"); assert!( no_matches(entries, "https://com/", None, None, false), "one" ); assert!( one_match(entries, "https://two.com/", None, None, 1, false), "two" ); assert!( one_match( entries, "https://two.com/other-page", None, None, 1, false ), "two" ); assert!( one_match( entries, "https://login.three.com/", None, None, 2, false ), "three" ); assert!( no_matches(entries, "https://three.com/", None, None, false), "three" ); assert!( one_match(entries, "https://four.com/", None, None, 3, false), "four" ); assert!( one_match( entries, "https://five.com:8080/", None, None, 4, false ), "five" ); assert!( no_matches(entries, "https://five.com/", None, None, false), "five" ); assert!( one_match(entries, "https://six.com:8080/", None, None, 5, false), "six" ); assert!( no_matches(entries, "https://six.com/", None, None, false), "six" ); assert!( one_match( entries, "https://192.168.0.128:8080/", None, None, 6, false ), "seven" ); assert!( no_matches(entries, "https://192.168.0.128/", None, None, false), "seven" ); } #[test] fn test_find_by_url_host() { let entries = &[ make_entry( "one", None, None, &[("https://one.com/", Some(rbw::api::UriMatchType::Host))], ), make_entry( "two", None, None, &[( "https://two.com/login", Some(rbw::api::UriMatchType::Host), )], ), make_entry( "three", None, None, &[( "https://login.three.com/", Some(rbw::api::UriMatchType::Host), )], ), make_entry( "four", None, None, &[("four.com", Some(rbw::api::UriMatchType::Host))], ), make_entry( "five", None, None, &[( "https://five.com:8080/", Some(rbw::api::UriMatchType::Host), )], ), make_entry( "six", None, None, &[("six.com:8080", Some(rbw::api::UriMatchType::Host))], ), make_entry( "seven", None, None, &[("192.168.0.128:8080", Some(rbw::api::UriMatchType::Host))], ), ]; assert!( one_match(entries, "https://one.com/", None, None, 0, false), "one" ); assert!( no_matches(entries, "https://login.one.com/", None, None, false), "one" ); assert!( one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); assert!(no_matches(entries, "one.com", None, None, false), "one"); assert!(no_matches(entries, "https", None, None, false), "one"); assert!(no_matches(entries, "com", None, None, false), "one"); assert!( no_matches(entries, "https://com/", None, None, false), "one" ); assert!( one_match(entries, "https://two.com/", None, None, 1, false), "two" ); assert!( one_match( entries, "https://two.com/other-page", None, None, 1, false ), "two" ); assert!( one_match( entries, "https://login.three.com/", None, None, 2, false ), "three" ); assert!( no_matches(entries, "https://three.com/", None, None, false), "three" ); assert!( one_match(entries, "https://four.com/", None, None, 3, false), "four" ); assert!( one_match( entries, "https://five.com:8080/", None, None, 4, false ), "five" ); assert!( no_matches(entries, "https://five.com/", None, None, false), "five" ); assert!( one_match(entries, "https://six.com:8080/", None, None, 5, false), "six" ); assert!( no_matches(entries, "https://six.com/", None, None, false), "six" ); assert!( one_match( entries, "https://192.168.0.128:8080/", None, None, 6, false ), "seven" ); assert!( no_matches(entries, "https://192.168.0.128/", None, None, false), "seven" ); } #[test] fn test_find_by_url_starts_with() { let entries = &[ make_entry( "one", None, None, &[( "https://one.com/", Some(rbw::api::UriMatchType::StartsWith), )], ), make_entry( "two", None, None, &[( "https://two.com/login", Some(rbw::api::UriMatchType::StartsWith), )], ), make_entry( "three", None, None, &[( "https://login.three.com/", Some(rbw::api::UriMatchType::StartsWith), )], ), ]; assert!( one_match(entries, "https://one.com/", None, None, 0, false), "one" ); assert!( no_matches(entries, "https://login.one.com/", None, None, false), "one" ); assert!( one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); assert!(no_matches(entries, "one.com", None, None, false), "one"); assert!(no_matches(entries, "https", None, None, false), "one"); assert!(no_matches(entries, "com", None, None, false), "one"); assert!( no_matches(entries, "https://com/", None, None, false), "one" ); assert!( one_match(entries, "https://two.com/login", None, None, 1, false), "two" ); assert!( one_match( entries, "https://two.com/login/sso", None, None, 1, false ), "two" ); assert!( no_matches(entries, "https://two.com/", None, None, false), "two" ); assert!( no_matches( entries, "https://two.com/other-page", None, None, false ), "two" ); assert!( one_match( entries, "https://login.three.com/", None, None, 2, false ), "three" ); assert!( no_matches(entries, "https://three.com/", None, None, false), "three" ); } #[test] fn test_find_by_url_exact() { let entries = &[ make_entry( "one", None, None, &[("https://one.com/", Some(rbw::api::UriMatchType::Exact))], ), make_entry( "two", None, None, &[( "https://two.com/login", Some(rbw::api::UriMatchType::Exact), )], ), make_entry( "three", None, None, &[( "https://login.three.com/", Some(rbw::api::UriMatchType::Exact), )], ), make_entry( "four", None, None, &[("https://four.com", Some(rbw::api::UriMatchType::Exact))], ), ]; assert!( one_match(entries, "https://one.com/", None, None, 0, false), "one" ); assert!( one_match(entries, "https://one.com", None, None, 0, false), "one" ); assert!( no_matches(entries, "https://one.com/foo", None, None, false), "one" ); assert!( no_matches(entries, "https://login.one.com/", None, None, false), "one" ); assert!( one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); assert!(no_matches(entries, "one.com", None, None, false), "one"); assert!(no_matches(entries, "https", None, None, false), "one"); assert!(no_matches(entries, "com", None, None, false), "one"); assert!( no_matches(entries, "https://com/", None, None, false), "one" ); assert!( one_match(entries, "https://two.com/login", None, None, 1, false), "two" ); assert!( no_matches( entries, "https://two.com/login/sso", None, None, false ), "two" ); assert!( no_matches(entries, "https://two.com/", None, None, false), "two" ); assert!( no_matches( entries, "https://two.com/other-page", None, None, false ), "two" ); assert!( one_match( entries, "https://login.three.com/", None, None, 2, false ), "three" ); assert!( no_matches(entries, "https://three.com/", None, None, false), "three" ); assert!( one_match(entries, "https://four.com/", None, None, 3, false), "four" ); assert!( one_match(entries, "https://four.com", None, None, 3, false), "four" ); assert!( no_matches(entries, "https://four.com/foo", None, None, false), "four" ); } #[test] fn test_find_by_url_regex() { let entries = &[ make_entry( "one", None, None, &[( r"^https://one\.com/$", Some(rbw::api::UriMatchType::RegularExpression), )], ), make_entry( "two", None, None, &[( r"^https://two\.com/(login|start)", Some(rbw::api::UriMatchType::RegularExpression), )], ), make_entry( "three", None, None, &[( r"^https://(login\.)?three\.com/$", Some(rbw::api::UriMatchType::RegularExpression), )], ), ]; assert!( one_match(entries, "https://one.com/", None, None, 0, false), "one" ); assert!( no_matches(entries, "https://login.one.com/", None, None, false), "one" ); assert!( one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); assert!(no_matches(entries, "one.com", None, None, false), "one"); assert!(no_matches(entries, "https", None, None, false), "one"); assert!(no_matches(entries, "com", None, None, false), "one"); assert!( no_matches(entries, "https://com/", None, None, false), "one" ); assert!( one_match(entries, "https://two.com/login", None, None, 1, false), "two" ); assert!( one_match(entries, "https://two.com/start", None, None, 1, false), "two" ); assert!( one_match( entries, "https://two.com/login/sso", None, None, 1, false ), "two" ); assert!( no_matches(entries, "https://two.com/", None, None, false), "two" ); assert!( no_matches( entries, "https://two.com/other-page", None, None, false ), "two" ); assert!( one_match( entries, "https://login.three.com/", None, None, 2, false ), "three" ); assert!( one_match(entries, "https://three.com/", None, None, 2, false), "three" ); assert!( no_matches(entries, "https://www.three.com/", None, None, false), "three" ); } #[test] fn test_find_by_url_never() { let entries = &[ make_entry( "one", None, None, &[("https://one.com/", Some(rbw::api::UriMatchType::Never))], ), make_entry( "two", None, None, &[( "https://two.com/login", Some(rbw::api::UriMatchType::Never), )], ), make_entry( "three", None, None, &[( "https://login.three.com/", Some(rbw::api::UriMatchType::Never), )], ), make_entry( "four", None, None, &[("four.com", Some(rbw::api::UriMatchType::Never))], ), make_entry( "five", None, None, &[( "https://five.com:8080/", Some(rbw::api::UriMatchType::Never), )], ), make_entry( "six", None, None, &[("six.com:8080", Some(rbw::api::UriMatchType::Never))], ), ]; assert!( no_matches(entries, "https://one.com/", None, None, false), "one" ); assert!( no_matches(entries, "https://login.one.com/", None, None, false), "one" ); assert!( no_matches(entries, "https://one.com:443/", None, None, false), "one" ); assert!(no_matches(entries, "one.com", None, None, false), "one"); assert!(no_matches(entries, "https", None, None, false), "one"); assert!(no_matches(entries, "com", None, None, false), "one"); assert!( no_matches(entries, "https://com/", None, None, false), "one" ); assert!( no_matches(entries, "https://two.com/", None, None, false), "two" ); assert!( no_matches( entries, "https://two.com/other-page", None, None, false ), "two" ); assert!( no_matches( entries, "https://login.three.com/", None, None, false ), "three" ); assert!( no_matches(entries, "https://three.com/", None, None, false), "three" ); assert!( no_matches(entries, "https://four.com/", None, None, false), "four" ); assert!( no_matches(entries, "https://five.com:8080/", None, None, false), "five" ); assert!( no_matches(entries, "https://five.com/", None, None, false), "five" ); assert!( no_matches(entries, "https://six.com:8080/", None, None, false), "six" ); assert!( no_matches(entries, "https://six.com/", None, None, false), "six" ); } #[track_caller] fn one_match( entries: &[(rbw::db::Entry, DecryptedCipher)], needle: &str, username: Option<&str>, folder: Option<&str>, idx: usize, ignore_case: bool, ) -> bool { entries_eq( &find_entry_raw( entries, &parse_needle(needle).unwrap(), username, folder, ignore_case, ) .unwrap(), &entries[idx], ) } #[track_caller] fn no_matches( entries: &[(rbw::db::Entry, DecryptedCipher)], needle: &str, username: Option<&str>, folder: Option<&str>, ignore_case: bool, ) -> bool { let res = find_entry_raw( entries, &parse_needle(needle).unwrap(), username, folder, ignore_case, ); if let Err(e) = res { format!("{e}").contains("no entry found") } else { false } } #[track_caller] fn many_matches( entries: &[(rbw::db::Entry, DecryptedCipher)], needle: &str, username: Option<&str>, folder: Option<&str>, ignore_case: bool, ) -> bool { let res = find_entry_raw( entries, &parse_needle(needle).unwrap(), username, folder, ignore_case, ); if let Err(e) = res { format!("{e}").contains("multiple entries found") } else { false } } #[track_caller] fn entries_eq( a: &(rbw::db::Entry, DecryptedCipher), b: &(rbw::db::Entry, DecryptedCipher), ) -> bool { a.0 == b.0 && a.1 == b.1 } fn make_entry( name: &str, username: Option<&str>, folder: Option<&str>, uris: &[(&str, Option)], ) -> (rbw::db::Entry, DecryptedCipher) { let id = uuid::Uuid::new_v4(); ( rbw::db::Entry { id: id.to_string(), org_id: None, folder: folder.map(|_| "encrypted folder name".to_string()), folder_id: None, name: "this is the encrypted name".to_string(), data: rbw::db::EntryData::Login { username: username.map(|_| { "this is the encrypted username".to_string() }), password: None, uris: uris .iter() .map(|(_, match_type)| rbw::db::Uri { uri: "this is the encrypted uri".to_string(), match_type: *match_type, }) .collect(), totp: None, }, fields: vec![], notes: None, history: vec![], key: None, }, DecryptedCipher { id: id.to_string(), folder: folder.map(std::string::ToString::to_string), name: name.to_string(), data: DecryptedData::Login { username: username.map(std::string::ToString::to_string), password: None, totp: None, uris: Some( uris.iter() .map(|(uri, match_type)| DecryptedUri { uri: (*uri).to_string(), match_type: *match_type, }) .collect(), ), }, fields: vec![], notes: None, history: vec![], }, ) } } rbw-1.13.2/src/bin/rbw/main.rs000064400000000000000000000354431046102023000141360ustar 00000000000000use std::io::Write as _; use anyhow::Context as _; use clap::{CommandFactory as _, Parser as _}; mod actions; mod commands; mod sock; #[derive(Debug, clap::Parser)] #[command(version, about = "Unofficial Bitwarden CLI")] enum Opt { #[command(about = "Get or set configuration options")] Config { #[command(subcommand)] config: Config, }, #[command( about = "Register this device with the Bitwarden server", long_about = "Register this device with the Bitwarden server\n\n\ The official Bitwarden server includes bot detection to prevent \ brute force attacks. In order to avoid being detected as bot \ traffic, you will need to use this command to log in with your \ personal API key (instead of your password) first before regular \ logins will work." )] Register, #[command(about = "Log in to the Bitwarden server")] Login, #[command(about = "Unlock the local Bitwarden database")] Unlock, #[command(about = "Check if the local Bitwarden database is unlocked")] Unlocked, #[command(about = "Update the local copy of the Bitwarden database")] Sync, #[command( about = "List all entries in the local Bitwarden database", visible_alias = "ls" )] List { #[arg( long, help = "Fields to display. \ Available options are id, name, user, folder. \ Multiple fields will be separated by tabs.", default_value = "name", use_value_delimiter = true )] fields: Vec, }, #[command(about = "Display the password for a given entry")] Get { #[arg(help = "Name, URI or UUID of the entry to display", value_parser = commands::parse_needle)] needle: commands::Needle, #[arg(help = "Username of the entry to display")] user: Option, #[arg(long, help = "Folder name to search in")] folder: Option, #[arg(short, long, help = "Field to get")] field: Option, #[arg(long, help = "Display the notes in addition to the password")] full: bool, #[structopt(long, help = "Display output as JSON")] raw: bool, #[cfg(feature = "clipboard")] #[structopt(long, help = "Copy result to clipboard")] clipboard: bool, #[structopt(short, long, help = "Ignore case")] ignorecase: bool, }, #[command(about = "Search for entries")] Search { #[arg(help = "Search term to locate entries")] term: String, #[arg(long, help = "Folder name to search in")] folder: Option, }, #[command( about = "Display the authenticator code for a given entry", visible_alias = "totp" )] Code { #[arg(help = "Name, URI or UUID of the entry to display", value_parser = commands::parse_needle)] needle: commands::Needle, #[arg(help = "Username of the entry to display")] user: Option, #[arg(long, help = "Folder name to search in")] folder: Option, #[cfg(feature = "clipboard")] #[structopt(long, help = "Copy result to clipboard")] clipboard: bool, #[arg(short, long, help = "Ignore case")] ignorecase: bool, }, #[command( about = "Add a new password to the database", long_about = "Add a new password to the database\n\n\ This command will open a text editor to enter \ the password and notes. The editor to use is determined \ by the value of the $VISUAL or $EDITOR environment variables. The first line will be saved as the password and the \ remainder will be saved as a note." )] Add { #[arg(help = "Name of the password entry")] name: String, #[arg(help = "Username for the password entry")] user: Option, #[arg( long, help = "URI for the password entry", number_of_values = 1 )] uri: Vec, #[arg(long, help = "Folder for the password entry")] folder: Option, }, #[command( about = "Generate a new password", long_about = "Generate a new password\n\n\ If given a password entry name, also save the generated \ password to the database.", visible_alias = "gen", group = clap::ArgGroup::new("password-type").args(&[ "no_symbols", "only_numbers", "nonconfusables", "diceware", ]) )] Generate { #[arg(help = "Length of the password to generate")] len: usize, #[arg(help = "Name of the password entry")] name: Option, #[arg(help = "Username for the password entry")] user: Option, #[arg( long, help = "URI for the password entry", number_of_values = 1 )] uri: Vec, #[arg(long, help = "Folder for the password entry")] folder: Option, #[arg( long = "no-symbols", help = "Generate a password with no special characters" )] no_symbols: bool, #[arg( long = "only-numbers", help = "Generate a password consisting of only numbers" )] only_numbers: bool, #[arg( long, help = "Generate a password without visually similar \ characters (useful for passwords intended to be \ written down)" )] nonconfusables: bool, #[arg( long, help = "Generate a password of multiple dictionary \ words chosen from the EFF word list. The len \ parameter for this option will set the number \ of words to generate, rather than characters." )] diceware: bool, }, #[command( about = "Modify an existing password", long_about = "Modify an existing password\n\n\ This command will open a text editor with the existing \ password and notes of the given entry for editing. \ The editor to use is determined by the value of the \ $VISUAL or $EDITOR environment variables. The first line \ will be saved as the password and the remainder will be saved \ as a note." )] Edit { #[arg(help = "Name or UUID of the password entry")] name: String, #[arg(help = "Username for the password entry")] user: Option, #[arg(long, help = "Folder name to search in")] folder: Option, #[arg(short, long, help = "Ignore case")] ignorecase: bool, }, #[command(about = "Remove a given entry", visible_alias = "rm")] Remove { #[arg(help = "Name or UUID of the password entry")] name: String, #[arg(help = "Username for the password entry")] user: Option, #[arg(long, help = "Folder name to search in")] folder: Option, #[arg(short, long, help = "Ignore case")] ignorecase: bool, }, #[command(about = "View the password history for a given entry")] History { #[arg(help = "Name or UUID of the password entry")] name: String, #[arg(help = "Username for the password entry")] user: Option, #[arg(long, help = "Folder name to search in")] folder: Option, #[arg(short, long, help = "Ignore case")] ignorecase: bool, }, #[command(about = "Lock the password database")] Lock, #[command(about = "Remove the local copy of the password database")] Purge, #[command(name = "stop-agent", about = "Terminate the background agent")] StopAgent, #[command( name = "gen-completions", about = "Generate completion script for the given shell" )] GenCompletions { shell: clap_complete::Shell }, } impl Opt { fn subcommand_name(&self) -> String { match self { Self::Config { config } => { format!("config {}", config.subcommand_name()) } Self::Register => "register".to_string(), Self::Login => "login".to_string(), Self::Unlock => "unlock".to_string(), Self::Unlocked => "unlocked".to_string(), Self::Sync => "sync".to_string(), Self::List { .. } => "list".to_string(), Self::Get { .. } => "get".to_string(), Self::Search { .. } => "search".to_string(), Self::Code { .. } => "code".to_string(), Self::Add { .. } => "add".to_string(), Self::Generate { .. } => "generate".to_string(), Self::Edit { .. } => "edit".to_string(), Self::Remove { .. } => "remove".to_string(), Self::History { .. } => "history".to_string(), Self::Lock => "lock".to_string(), Self::Purge => "purge".to_string(), Self::StopAgent => "stop-agent".to_string(), Self::GenCompletions { .. } => "gen-completions".to_string(), } } } #[derive(Debug, clap::Parser)] enum Config { #[command(about = "Show the values of all configuration settings")] Show, #[command(about = "Set a configuration option")] Set { #[arg(help = "Configuration key to set")] key: String, #[arg(help = "Value to set the configuration option to")] value: String, }, #[command(about = "Reset a configuration option to its default")] Unset { #[arg(help = "Configuration key to unset")] key: String, }, } impl Config { fn subcommand_name(&self) -> String { match self { Self::Show => "show", Self::Set { .. } => "set", Self::Unset { .. } => "unset", } .to_string() } } fn main() { let opt = Opt::parse(); env_logger::Builder::from_env( env_logger::Env::default().default_filter_or("info"), ) .format(|buf, record| { if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() { let out = format!("{}: {}", record.level(), record.args()); writeln!(buf, "{}", textwrap::fill(&out, usize::from(w) - 1)) } else { writeln!(buf, "{}: {}", record.level(), record.args()) } }) .init(); let res = match &opt { Opt::Config { config } => match config { Config::Show => commands::config_show(), Config::Set { key, value } => commands::config_set(key, value), Config::Unset { key } => commands::config_unset(key), }, Opt::Register => commands::register(), Opt::Login => commands::login(), Opt::Unlock => commands::unlock(), Opt::Unlocked => commands::unlocked(), Opt::Sync => commands::sync(), Opt::List { fields } => commands::list(fields), Opt::Get { needle, user, folder, field, full, raw, #[cfg(feature = "clipboard")] clipboard, ignorecase, } => commands::get( needle, user.as_deref(), folder.as_deref(), field.as_deref(), *full, *raw, #[cfg(feature = "clipboard")] *clipboard, #[cfg(not(feature = "clipboard"))] false, *ignorecase, ), Opt::Search { term, folder } => { commands::search(term, folder.as_deref()) } Opt::Code { needle, user, folder, #[cfg(feature = "clipboard")] clipboard, ignorecase, } => commands::code( needle, user.as_deref(), folder.as_deref(), #[cfg(feature = "clipboard")] *clipboard, #[cfg(not(feature = "clipboard"))] false, *ignorecase, ), Opt::Add { name, user, uri, folder, } => commands::add( name, user.as_deref(), &uri.iter() // XXX not sure what the ui for specifying the match type // should be .map(|uri| (uri.clone(), None)) .collect::>(), folder.as_deref(), ), Opt::Generate { len, name, user, uri, folder, no_symbols, only_numbers, nonconfusables, diceware, } => { let ty = if *no_symbols { rbw::pwgen::Type::NoSymbols } else if *only_numbers { rbw::pwgen::Type::Numbers } else if *nonconfusables { rbw::pwgen::Type::NonConfusables } else if *diceware { rbw::pwgen::Type::Diceware } else { rbw::pwgen::Type::AllChars }; commands::generate( name.as_deref(), user.as_deref(), &uri.iter() // XXX not sure what the ui for specifying the match type // should be .map(|uri| (uri.clone(), None)) .collect::>(), folder.as_deref(), *len, ty, ) } Opt::Edit { name, user, folder, ignorecase, } => commands::edit( name, user.as_deref(), folder.as_deref(), *ignorecase, ), Opt::Remove { name, user, folder, ignorecase, } => commands::remove( name, user.as_deref(), folder.as_deref(), *ignorecase, ), Opt::History { name, user, folder, ignorecase, } => commands::history( name, user.as_deref(), folder.as_deref(), *ignorecase, ), Opt::Lock => commands::lock(), Opt::Purge => commands::purge(), Opt::StopAgent => commands::stop_agent(), Opt::GenCompletions { shell } => { clap_complete::generate( *shell, &mut Opt::command(), "rbw", &mut std::io::stdout(), ); Ok(()) } } .context(format!("rbw {}", opt.subcommand_name())); if let Err(e) = res { eprintln!("{e:#}"); std::process::exit(1); } } rbw-1.13.2/src/bin/rbw/sock.rs000064400000000000000000000024031046102023000141370ustar 00000000000000use std::io::{BufRead as _, Write as _}; use anyhow::Context as _; pub struct Sock(std::os::unix::net::UnixStream); impl Sock { // not returning anyhow::Result here because we want to be able to handle // specific kinds of std::io::Results differently pub fn connect() -> std::io::Result { Ok(Self(std::os::unix::net::UnixStream::connect( rbw::dirs::socket_file(), )?)) } pub fn send( &mut self, msg: &rbw::protocol::Request, ) -> anyhow::Result<()> { let Self(sock) = self; sock.write_all( serde_json::to_string(msg) .context("failed to serialize message to agent")? .as_bytes(), ) .context("failed to send message to agent")?; sock.write_all(b"\n") .context("failed to send message to agent")?; Ok(()) } pub fn recv(&mut self) -> anyhow::Result { let Self(sock) = self; let mut buf = std::io::BufReader::new(sock); let mut line = String::new(); buf.read_line(&mut line) .context("failed to read message from agent")?; serde_json::from_str(&line) .context("failed to parse message from agent") } } rbw-1.13.2/src/bin/rbw-agent/actions.rs000064400000000000000000000542321046102023000157430ustar 00000000000000use anyhow::Context as _; pub async fn register( sock: &mut crate::sock::Sock, environment: &rbw::protocol::Environment, ) -> anyhow::Result<()> { let db = load_db().await.unwrap_or_else(|_| rbw::db::Db::new()); if db.needs_login() { let url_str = config_base_url().await?; let url = reqwest::Url::parse(&url_str) .context("failed to parse base url")?; let Some(host) = url.host_str() else { return Err(anyhow::anyhow!( "couldn't find host in rbw base url {}", url_str )); }; let email = config_email().await?; let mut err_msg = None; for i in 1_u8..=3 { let err = if i > 1 { // this unwrap is safe because we only ever continue the loop // if we have set err_msg Some(format!("{} (attempt {}/3)", err_msg.unwrap(), i)) } else { None }; let client_id = rbw::pinentry::getpin( &config_pinentry().await?, "API key client__id", &format!("Log in to {host}"), err.as_deref(), environment, false, ) .await .context("failed to read client_id from pinentry")?; let client_secret = rbw::pinentry::getpin( &config_pinentry().await?, "API key client__secret", &format!("Log in to {host}"), err.as_deref(), environment, false, ) .await .context("failed to read client_secret from pinentry")?; let apikey = rbw::locked::ApiKey::new(client_id, client_secret); match rbw::actions::register(&email, apikey.clone()).await { Ok(()) => { break; } Err(rbw::error::Error::IncorrectPassword { message }) => { if i == 3 { return Err(rbw::error::Error::IncorrectPassword { message, }) .context("failed to log in to bitwarden instance"); } err_msg = Some(message); continue; } Err(e) => { return Err(e) .context("failed to log in to bitwarden instance") } } } } respond_ack(sock).await?; Ok(()) } pub async fn login( sock: &mut crate::sock::Sock, state: std::sync::Arc>, environment: &rbw::protocol::Environment, ) -> anyhow::Result<()> { let db = load_db().await.unwrap_or_else(|_| rbw::db::Db::new()); if db.needs_login() { let url_str = config_base_url().await?; let url = reqwest::Url::parse(&url_str) .context("failed to parse base url")?; let Some(host) = url.host_str() else { return Err(anyhow::anyhow!( "couldn't find host in rbw base url {}", url_str )); }; let email = config_email().await?; let mut err_msg = None; 'attempts: for i in 1_u8..=3 { let err = if i > 1 { // this unwrap is safe because we only ever continue the loop // if we have set err_msg Some(format!("{} (attempt {}/3)", err_msg.unwrap(), i)) } else { None }; let password = rbw::pinentry::getpin( &config_pinentry().await?, "Master Password", &format!("Log in to {host}"), err.as_deref(), environment, true, ) .await .context("failed to read password from pinentry")?; match rbw::actions::login(&email, password.clone(), None, None) .await { Ok(( access_token, refresh_token, kdf, iterations, memory, parallelism, protected_key, )) => { login_success( state.clone(), access_token, refresh_token, kdf, iterations, memory, parallelism, protected_key, password, db, email, ) .await?; break 'attempts; } Err(rbw::error::Error::TwoFactorRequired { providers }) => { let supported_types = vec![ rbw::api::TwoFactorProviderType::Authenticator, rbw::api::TwoFactorProviderType::Yubikey, rbw::api::TwoFactorProviderType::Email, ]; for provider in supported_types { if providers.contains(&provider) { let ( access_token, refresh_token, kdf, iterations, memory, parallelism, protected_key, ) = two_factor( environment, &email, password.clone(), provider, ) .await?; login_success( state.clone(), access_token, refresh_token, kdf, iterations, memory, parallelism, protected_key, password, db, email, ) .await?; break 'attempts; } } return Err(anyhow::anyhow!( "unsupported two factor methods: {providers:?}" )); } Err(rbw::error::Error::IncorrectPassword { message }) => { if i == 3 { return Err(rbw::error::Error::IncorrectPassword { message, }) .context("failed to log in to bitwarden instance"); } err_msg = Some(message); continue; } Err(e) => { return Err(e) .context("failed to log in to bitwarden instance") } } } } respond_ack(sock).await?; Ok(()) } async fn two_factor( environment: &rbw::protocol::Environment, email: &str, password: rbw::locked::Password, provider: rbw::api::TwoFactorProviderType, ) -> anyhow::Result<( String, String, rbw::api::KdfType, u32, Option, Option, String, )> { let mut err_msg = None; for i in 1_u8..=3 { let err = if i > 1 { // this unwrap is safe because we only ever continue the loop if // we have set err_msg Some(format!("{} (attempt {}/3)", err_msg.unwrap(), i)) } else { None }; let code = rbw::pinentry::getpin( &config_pinentry().await?, provider.header(), provider.message(), err.as_deref(), environment, provider.grab(), ) .await .context("failed to read code from pinentry")?; let code = std::str::from_utf8(code.password()) .context("code was not valid utf8")?; match rbw::actions::login( email, password.clone(), Some(code), Some(provider), ) .await { Ok(( access_token, refresh_token, kdf, iterations, memory, parallelism, protected_key, )) => { return Ok(( access_token, refresh_token, kdf, iterations, memory, parallelism, protected_key, )) } Err(rbw::error::Error::IncorrectPassword { message }) => { if i == 3 { return Err(rbw::error::Error::IncorrectPassword { message, }) .context("failed to log in to bitwarden instance"); } err_msg = Some(message); continue; } // can get this if the user passes an empty string Err(rbw::error::Error::TwoFactorRequired { .. }) => { let message = "TOTP code is not a number".to_string(); if i == 3 { return Err(rbw::error::Error::IncorrectPassword { message, }) .context("failed to log in to bitwarden instance"); } err_msg = Some(message); continue; } Err(e) => { return Err(e) .context("failed to log in to bitwarden instance") } } } unreachable!() } async fn login_success( state: std::sync::Arc>, access_token: String, refresh_token: String, kdf: rbw::api::KdfType, iterations: u32, memory: Option, parallelism: Option, protected_key: String, password: rbw::locked::Password, mut db: rbw::db::Db, email: String, ) -> anyhow::Result<()> { db.access_token = Some(access_token.to_string()); db.refresh_token = Some(refresh_token.to_string()); db.kdf = Some(kdf); db.iterations = Some(iterations); db.memory = memory; db.parallelism = parallelism; db.protected_key = Some(protected_key.to_string()); save_db(&db).await?; sync(None, state.clone()).await?; let db = load_db().await?; let Some(protected_private_key) = db.protected_private_key else { return Err(anyhow::anyhow!( "failed to find protected private key in db" )); }; let res = rbw::actions::unlock( &email, &password, kdf, iterations, memory, parallelism, &protected_key, &protected_private_key, &db.protected_org_keys, ); match res { Ok((keys, org_keys)) => { let mut state = state.lock().await; state.priv_key = Some(keys); state.org_keys = Some(org_keys); } Err(e) => return Err(e).context("failed to unlock database"), } Ok(()) } pub async fn unlock( sock: &mut crate::sock::Sock, state: std::sync::Arc>, environment: &rbw::protocol::Environment, ) -> anyhow::Result<()> { if state.lock().await.needs_unlock() { let db = load_db().await?; let Some(kdf) = db.kdf else { return Err(anyhow::anyhow!("failed to find kdf type in db")); }; let Some(iterations) = db.iterations else { return Err(anyhow::anyhow!( "failed to find number of iterations in db" )); }; let memory = db.memory; let parallelism = db.parallelism; let Some(protected_key) = db.protected_key else { return Err(anyhow::anyhow!( "failed to find protected key in db" )); }; let Some(protected_private_key) = db.protected_private_key else { return Err(anyhow::anyhow!( "failed to find protected private key in db" )); }; let email = config_email().await?; let mut err_msg = None; for i in 1_u8..=3 { let err = if i > 1 { // this unwrap is safe because we only ever continue the loop // if we have set err_msg Some(format!("{} (attempt {}/3)", err_msg.unwrap(), i)) } else { None }; let password = rbw::pinentry::getpin( &config_pinentry().await?, "Master Password", &format!( "Unlock the local database for '{}'", rbw::dirs::profile() ), err.as_deref(), environment, true, ) .await .context("failed to read password from pinentry")?; match rbw::actions::unlock( &email, &password, kdf, iterations, memory, parallelism, &protected_key, &protected_private_key, &db.protected_org_keys, ) { Ok((keys, org_keys)) => { unlock_success(state, keys, org_keys).await?; break; } Err(rbw::error::Error::IncorrectPassword { message }) => { if i == 3 { return Err(rbw::error::Error::IncorrectPassword { message, }) .context("failed to unlock database"); } err_msg = Some(message); continue; } Err(e) => return Err(e).context("failed to unlock database"), } } } respond_ack(sock).await?; Ok(()) } async fn unlock_success( state: std::sync::Arc>, keys: rbw::locked::Keys, org_keys: std::collections::HashMap, ) -> anyhow::Result<()> { let mut state = state.lock().await; state.priv_key = Some(keys); state.org_keys = Some(org_keys); Ok(()) } pub async fn lock( sock: &mut crate::sock::Sock, state: std::sync::Arc>, ) -> anyhow::Result<()> { state.lock().await.clear(); respond_ack(sock).await?; Ok(()) } pub async fn check_lock( sock: &mut crate::sock::Sock, state: std::sync::Arc>, ) -> anyhow::Result<()> { if state.lock().await.needs_unlock() { return Err(anyhow::anyhow!("agent is locked")); } respond_ack(sock).await?; Ok(()) } pub async fn sync( sock: Option<&mut crate::sock::Sock>, state: std::sync::Arc>, ) -> anyhow::Result<()> { let mut db = load_db().await?; let access_token = if let Some(access_token) = &db.access_token { access_token.clone() } else { return Err(anyhow::anyhow!("failed to find access token in db")); }; let refresh_token = if let Some(refresh_token) = &db.refresh_token { refresh_token.clone() } else { return Err(anyhow::anyhow!("failed to find refresh token in db")); }; let ( access_token, (protected_key, protected_private_key, protected_org_keys, entries), ) = rbw::actions::sync(&access_token, &refresh_token) .await .context("failed to sync database from server")?; if let Some(access_token) = access_token { db.access_token = Some(access_token); } db.protected_key = Some(protected_key); db.protected_private_key = Some(protected_private_key); db.protected_org_keys = protected_org_keys; db.entries = entries; save_db(&db).await?; if let Err(e) = subscribe_to_notifications(state.clone()).await { eprintln!("failed to subscribe to notifications: {e}"); } if let Some(sock) = sock { respond_ack(sock).await?; } Ok(()) } pub async fn decrypt( sock: &mut crate::sock::Sock, state: std::sync::Arc>, cipherstring: &str, entry_key: Option<&str>, org_id: Option<&str>, ) -> anyhow::Result<()> { let state = state.lock().await; let Some(keys) = state.key(org_id) else { return Err(anyhow::anyhow!( "failed to find decryption keys in in-memory state" )); }; let entry_key = if let Some(entry_key) = entry_key { let key_cipherstring = rbw::cipherstring::CipherString::new(entry_key) .context("failed to parse individual item encryption key")?; Some(rbw::locked::Keys::new( key_cipherstring.decrypt_locked_symmetric(keys).context( "failed to decrypt individual item encryption key", )?, )) } else { None }; let cipherstring = rbw::cipherstring::CipherString::new(cipherstring) .context("failed to parse encrypted secret")?; let plaintext = String::from_utf8( cipherstring .decrypt_symmetric(keys, entry_key.as_ref()) .context("failed to decrypt encrypted secret")?, ) .context("failed to parse decrypted secret")?; respond_decrypt(sock, plaintext).await?; Ok(()) } pub async fn encrypt( sock: &mut crate::sock::Sock, state: std::sync::Arc>, plaintext: &str, org_id: Option<&str>, ) -> anyhow::Result<()> { let state = state.lock().await; let Some(keys) = state.key(org_id) else { return Err(anyhow::anyhow!( "failed to find encryption keys in in-memory state" )); }; let cipherstring = rbw::cipherstring::CipherString::encrypt_symmetric( keys, plaintext.as_bytes(), ) .context("failed to encrypt plaintext secret")?; respond_encrypt(sock, cipherstring.to_string()).await?; Ok(()) } #[cfg(feature = "clipboard")] pub async fn clipboard_store( sock: &mut crate::sock::Sock, state: std::sync::Arc>, text: &str, ) -> anyhow::Result<()> { let mut state = state.lock().await; if let Some(clipboard) = &mut state.clipboard { clipboard.set_text(text).map_err(|e| { anyhow::anyhow!("couldn't store value to clipboard: {e}") })?; } respond_ack(sock).await?; Ok(()) } #[cfg(not(feature = "clipboard"))] pub async fn clipboard_store( sock: &mut crate::sock::Sock, _state: std::sync::Arc>, _text: &str, ) -> anyhow::Result<()> { sock.send(&rbw::protocol::Response::Error { error: "clipboard not supported".to_string(), }) .await?; Ok(()) } pub async fn version(sock: &mut crate::sock::Sock) -> anyhow::Result<()> { sock.send(&rbw::protocol::Response::Version { version: rbw::protocol::version(), }) .await?; Ok(()) } async fn respond_ack(sock: &mut crate::sock::Sock) -> anyhow::Result<()> { sock.send(&rbw::protocol::Response::Ack).await?; Ok(()) } async fn respond_decrypt( sock: &mut crate::sock::Sock, plaintext: String, ) -> anyhow::Result<()> { sock.send(&rbw::protocol::Response::Decrypt { plaintext }) .await?; Ok(()) } async fn respond_encrypt( sock: &mut crate::sock::Sock, cipherstring: String, ) -> anyhow::Result<()> { sock.send(&rbw::protocol::Response::Encrypt { cipherstring }) .await?; Ok(()) } async fn config_email() -> anyhow::Result { let config = rbw::config::Config::load_async().await?; config.email.map_or_else( || Err(anyhow::anyhow!("failed to find email address in config")), Ok, ) } async fn load_db() -> anyhow::Result { let config = rbw::config::Config::load_async().await?; if let Some(email) = &config.email { rbw::db::Db::load_async(&config.server_name(), email) .await .map_err(anyhow::Error::new) } else { Err(anyhow::anyhow!("failed to find email address in config")) } } async fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> { let config = rbw::config::Config::load_async().await?; if let Some(email) = &config.email { db.save_async(&config.server_name(), email) .await .map_err(anyhow::Error::new) } else { Err(anyhow::anyhow!("failed to find email address in config")) } } async fn config_base_url() -> anyhow::Result { let config = rbw::config::Config::load_async().await?; Ok(config.base_url()) } async fn config_pinentry() -> anyhow::Result { let config = rbw::config::Config::load_async().await?; Ok(config.pinentry) } pub async fn subscribe_to_notifications( state: std::sync::Arc>, ) -> anyhow::Result<()> { if state.lock().await.notifications_handler.is_connected() { return Ok(()); } let config = rbw::config::Config::load_async() .await .context("Config is missing")?; let email = config.email.clone().context("Config is missing email")?; let db = rbw::db::Db::load_async(config.server_name().as_str(), &email) .await?; let access_token = db.access_token.context("Error getting access token")?; let websocket_url = format!( "{}/hub?access_token={}", config.notifications_url(), access_token ) .replace("https://", "wss://"); let mut state = state.lock().await; state .notifications_handler .connect(websocket_url) .await .err() .map_or_else(|| Ok(()), |err| Err(anyhow::anyhow!(err.to_string()))) } rbw-1.13.2/src/bin/rbw-agent/agent.rs000064400000000000000000000177011046102023000154010ustar 00000000000000use anyhow::Context as _; use futures_util::StreamExt as _; pub struct State { pub priv_key: Option, pub org_keys: Option>, pub timeout: crate::timeout::Timeout, pub timeout_duration: std::time::Duration, pub sync_timeout: crate::timeout::Timeout, pub sync_timeout_duration: std::time::Duration, pub notifications_handler: crate::notifications::Handler, #[cfg(feature = "clipboard")] pub clipboard: Option, } impl State { pub fn key(&self, org_id: Option<&str>) -> Option<&rbw::locked::Keys> { org_id.map_or(self.priv_key.as_ref(), |id| { self.org_keys.as_ref().and_then(|h| h.get(id)) }) } pub fn needs_unlock(&self) -> bool { self.priv_key.is_none() || self.org_keys.is_none() } pub fn set_timeout(&self) { self.timeout.set(self.timeout_duration); } pub fn clear(&mut self) { self.priv_key = None; self.org_keys = None; self.timeout.clear(); } pub fn set_sync_timeout(&self) { self.sync_timeout.set(self.sync_timeout_duration); } } pub struct Agent { timer_r: tokio::sync::mpsc::UnboundedReceiver<()>, sync_timer_r: tokio::sync::mpsc::UnboundedReceiver<()>, state: std::sync::Arc>, } impl Agent { pub fn new() -> anyhow::Result { let config = rbw::config::Config::load()?; let timeout_duration = std::time::Duration::from_secs(config.lock_timeout); let sync_timeout_duration = std::time::Duration::from_secs(config.sync_interval); let (timeout, timer_r) = crate::timeout::Timeout::new(); let (sync_timeout, sync_timer_r) = crate::timeout::Timeout::new(); if sync_timeout_duration > std::time::Duration::ZERO { sync_timeout.set(sync_timeout_duration); } let notifications_handler = crate::notifications::Handler::new(); Ok(Self { timer_r, sync_timer_r, state: std::sync::Arc::new(tokio::sync::Mutex::new(State { priv_key: None, org_keys: None, timeout, timeout_duration, sync_timeout, sync_timeout_duration, notifications_handler, #[cfg(feature = "clipboard")] clipboard: arboard::Clipboard::new() .inspect_err(|e| { log::warn!("couldn't create clipboard context: {e}"); }) .ok(), })), }) } pub async fn run( self, listener: tokio::net::UnixListener, ) -> anyhow::Result<()> { pub enum Event { Request(std::io::Result), Timeout(()), Sync(()), } let notifications = self .state .lock() .await .notifications_handler .get_channel() .await; let notifications = tokio_stream::wrappers::UnboundedReceiverStream::new( notifications, ) .map(|message| match message { crate::notifications::Message::Logout => Event::Timeout(()), crate::notifications::Message::Sync => Event::Sync(()), }) .boxed(); let mut stream = futures_util::stream::select_all([ tokio_stream::wrappers::UnixListenerStream::new(listener) .map(Event::Request) .boxed(), tokio_stream::wrappers::UnboundedReceiverStream::new( self.timer_r, ) .map(Event::Timeout) .boxed(), tokio_stream::wrappers::UnboundedReceiverStream::new( self.sync_timer_r, ) .map(Event::Sync) .boxed(), notifications, ]); while let Some(event) = stream.next().await { match event { Event::Request(res) => { let mut sock = crate::sock::Sock::new( res.context("failed to accept incoming connection")?, ); let state = self.state.clone(); tokio::spawn(async move { let res = handle_request(&mut sock, state.clone()).await; if let Err(e) = res { // unwrap is the only option here sock.send(&rbw::protocol::Response::Error { error: format!("{e:#}"), }) .await .unwrap(); } }); } Event::Timeout(()) => { self.state.lock().await.clear(); } Event::Sync(()) => { let state = self.state.clone(); tokio::spawn(async move { // this could fail if we aren't logged in, but we // don't care about that if let Err(e) = crate::actions::sync(None, state.clone()).await { eprintln!("failed to sync: {e:#}"); } }); self.state.lock().await.set_sync_timeout(); } } } Ok(()) } } async fn handle_request( sock: &mut crate::sock::Sock, state: std::sync::Arc>, ) -> anyhow::Result<()> { let req = sock.recv().await?; let req = match req { Ok(msg) => msg, Err(error) => { sock.send(&rbw::protocol::Response::Error { error }).await?; return Ok(()); } }; let set_timeout = match &req.action { rbw::protocol::Action::Register => { crate::actions::register(sock, &req.environment()).await?; true } rbw::protocol::Action::Login => { crate::actions::login(sock, state.clone(), &req.environment()) .await?; true } rbw::protocol::Action::Unlock => { crate::actions::unlock(sock, state.clone(), &req.environment()) .await?; true } rbw::protocol::Action::CheckLock => { crate::actions::check_lock(sock, state.clone()).await?; false } rbw::protocol::Action::Lock => { crate::actions::lock(sock, state.clone()).await?; false } rbw::protocol::Action::Sync => { crate::actions::sync(Some(sock), state.clone()).await?; false } rbw::protocol::Action::Decrypt { cipherstring, entry_key, org_id, } => { crate::actions::decrypt( sock, state.clone(), cipherstring, entry_key.as_deref(), org_id.as_deref(), ) .await?; true } rbw::protocol::Action::Encrypt { plaintext, org_id } => { crate::actions::encrypt( sock, state.clone(), plaintext, org_id.as_deref(), ) .await?; true } rbw::protocol::Action::ClipboardStore { text } => { crate::actions::clipboard_store(sock, state.clone(), text) .await?; true } rbw::protocol::Action::Quit => std::process::exit(0), rbw::protocol::Action::Version => { crate::actions::version(sock).await?; false } }; if set_timeout { state.lock().await.set_timeout(); } Ok(()) } rbw-1.13.2/src/bin/rbw-agent/daemon.rs000064400000000000000000000033571046102023000155500ustar 00000000000000pub struct StartupAck { writer: std::os::unix::io::OwnedFd, } impl StartupAck { pub fn ack(self) -> anyhow::Result<()> { rustix::io::write(&self.writer, &[0])?; Ok(()) } } pub fn daemonize() -> anyhow::Result { let stdout = std::fs::OpenOptions::new() .append(true) .create(true) .open(rbw::dirs::agent_stdout_file())?; let stderr = std::fs::OpenOptions::new() .append(true) .create(true) .open(rbw::dirs::agent_stderr_file())?; let (r, w) = rustix::pipe::pipe()?; let daemonize = daemonize::Daemonize::new() .pid_file(rbw::dirs::pid_file()) .stdout(stdout) .stderr(stderr); let res = match daemonize.execute() { daemonize::Outcome::Parent(_) => { drop(w); let mut buf = [0; 1]; // unwraps are necessary because not really a good way to handle // errors here otherwise rustix::io::read(&r, &mut buf).unwrap(); drop(r); std::process::exit(0); } daemonize::Outcome::Child(res) => res, }; drop(r); match res { Ok(_) => (), Err(e) => { // XXX super gross, but daemonize removed the ability to match // on specific error types for some reason? if e.to_string().contains("unable to lock pid file") { // this means that there is already an agent running, so // return a special exit code to allow the cli to detect // this case and not error out std::process::exit(23); } else { panic!("failed to daemonize: {e}"); } } } Ok(StartupAck { writer: w }) } rbw-1.13.2/src/bin/rbw-agent/debugger.rs000064400000000000000000000017161046102023000160660ustar 00000000000000// Prevent other user processes from attaching to the rbw agent and dumping // memory This is not perfect protection, but closes a door. Unfortunately, // prctl only works on Linux. #[cfg(target_os = "linux")] pub fn disable_tracing() -> anyhow::Result<()> { // https://github.com/torvalds/linux/blob/v5.11/include/uapi/linux/prctl.h#L14 const PR_SET_DUMPABLE: i32 = 4; // safe because it's just a raw call to prctl, and the arguments are // correct let ret = unsafe { libc::prctl(PR_SET_DUMPABLE, 0) }; if ret == 0 { Ok(()) } else { let e = std::io::Error::last_os_error(); Err(anyhow::anyhow!("failed to disable PTRACE_ATTACH, agent memory may be dumpable by other processes: {}", e)) } } #[cfg(not(target_os = "linux"))] pub fn disable_tracing() -> anyhow::Result<()> { Err(anyhow::anyhow!("failed to disable PTRACE_ATTACH, agent memory may be dumpable by other processes: unimplemented on this platform")) } rbw-1.13.2/src/bin/rbw-agent/main.rs000064400000000000000000000032321046102023000152210ustar 00000000000000use anyhow::Context as _; mod actions; mod agent; mod daemon; mod debugger; mod notifications; mod sock; mod timeout; async fn tokio_main( startup_ack: Option, ) -> anyhow::Result<()> { let listener = crate::sock::listen()?; if let Some(startup_ack) = startup_ack { startup_ack.ack()?; } let agent = crate::agent::Agent::new()?; agent.run(listener).await?; Ok(()) } fn real_main() -> anyhow::Result<()> { env_logger::Builder::from_env( env_logger::Env::default().default_filter_or("info"), ) .init(); let no_daemonize = std::env::args() .nth(1) .map_or(false, |arg| arg == "--no-daemonize"); rbw::dirs::make_all()?; let startup_ack = if no_daemonize { None } else { Some(daemon::daemonize().context("failed to daemonize")?) }; if let Err(e) = debugger::disable_tracing() { log::warn!("{}", e); } let (w, r) = std::sync::mpsc::channel(); // can't use tokio::main because we need to daemonize before starting the // tokio runloop, or else things break // unwrap is fine here because there's no good reason that this should // ever fail tokio::runtime::Runtime::new().unwrap().block_on(async { if let Err(e) = tokio_main(startup_ack).await { // this unwrap is fine because it's the only real option here w.send(e).unwrap(); } }); if let Ok(e) = r.recv() { return Err(e); } Ok(()) } fn main() { let res = real_main(); if let Err(e) = res { // XXX log file? eprintln!("{e:#}"); std::process::exit(1); } } rbw-1.13.2/src/bin/rbw-agent/notifications.rs000064400000000000000000000116771046102023000171620ustar 00000000000000use futures_util::{SinkExt as _, StreamExt as _}; #[derive(Clone, Copy, Debug)] pub enum Message { Sync, Logout, } pub struct Handler { write: Option< futures::stream::SplitSink< tokio_tungstenite::WebSocketStream< tokio_tungstenite::MaybeTlsStream, >, tokio_tungstenite::tungstenite::Message, >, >, read_handle: Option>, sending_channels: std::sync::Arc< tokio::sync::RwLock>>, >, } impl Handler { pub fn new() -> Self { Self { write: None, read_handle: None, sending_channels: std::sync::Arc::new(tokio::sync::RwLock::new( Vec::new(), )), } } pub async fn connect( &mut self, url: String, ) -> Result<(), Box> { if self.is_connected() { self.disconnect().await?; } let (write, read_handle) = subscribe_to_notifications(url, self.sending_channels.clone()) .await?; self.write = Some(write); self.read_handle = Some(read_handle); Ok(()) } pub fn is_connected(&self) -> bool { self.write.is_some() && self.read_handle.is_some() && !self.read_handle.as_ref().unwrap().is_finished() } pub async fn disconnect( &mut self, ) -> Result<(), Box> { self.sending_channels.write().await.clear(); if let Some(mut write) = self.write.take() { write .send(tokio_tungstenite::tungstenite::Message::Close(None)) .await?; write.close().await?; self.read_handle.take().unwrap().await?; } self.write = None; self.read_handle = None; Ok(()) } pub async fn get_channel( &self, ) -> tokio::sync::mpsc::UnboundedReceiver { let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); self.sending_channels.write().await.push(tx); rx } } async fn subscribe_to_notifications( url: String, sending_channels: std::sync::Arc< tokio::sync::RwLock>>, >, ) -> Result< ( futures_util::stream::SplitSink< tokio_tungstenite::WebSocketStream< tokio_tungstenite::MaybeTlsStream, >, tokio_tungstenite::tungstenite::Message, >, tokio::task::JoinHandle<()>, ), Box, > { let url = url::Url::parse(url.as_str())?; let (ws_stream, _response) = tokio_tungstenite::connect_async(url).await?; let (mut write, read) = ws_stream.split(); write .send(tokio_tungstenite::tungstenite::Message::Text( "{\"protocol\":\"messagepack\",\"version\":1}\x1e".to_string(), )) .await .unwrap(); let read_future = async move { let sending_channels = &sending_channels; read.for_each(|message| async move { match message { Ok(message) => { if let Some(message) = parse_message(message) { let sending_channels = sending_channels.read().await; let sending_channels = sending_channels.as_slice(); for channel in sending_channels { channel.send(message).unwrap(); } } } Err(e) => { eprintln!("websocket error: {e:?}"); } } }) .await; }; Ok((write, tokio::spawn(read_future))) } fn parse_message( message: tokio_tungstenite::tungstenite::Message, ) -> Option { let tokio_tungstenite::tungstenite::Message::Binary(data) = message else { return None; }; // the first few bytes with the 0x80 bit set, plus one byte terminating the length contain the length of the message let len_buffer_length = data.iter().position(|&x| (x & 0x80) == 0)? + 1; let unpacked_messagepack = rmpv::decode::read_value(&mut &data[len_buffer_length..]).ok()?; let unpacked_message = unpacked_messagepack.as_array()?; let message_type = unpacked_message.first()?.as_u64()?; // invocation if message_type != 1 { return None; } let target = unpacked_message.get(3)?.as_str()?; if target != "ReceiveMessage" { return None; } let args = unpacked_message.get(4)?.as_array()?; let map = args.first()?.as_map()?; for (k, v) in map { if k.as_str()? == "Type" { let ty = v.as_i64()?; return match ty { 11 => Some(Message::Logout), _ => Some(Message::Sync), }; } } None } rbw-1.13.2/src/bin/rbw-agent/sock.rs000064400000000000000000000032421046102023000152350ustar 00000000000000use anyhow::Context as _; use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _}; pub struct Sock(tokio::net::UnixStream); impl Sock { pub fn new(s: tokio::net::UnixStream) -> Self { Self(s) } pub async fn send( &mut self, res: &rbw::protocol::Response, ) -> anyhow::Result<()> { if let rbw::protocol::Response::Error { error } = res { log::warn!("{error}"); } let Self(sock) = self; sock.write_all( serde_json::to_string(res) .context("failed to serialize message")? .as_bytes(), ) .await .context("failed to write message to socket")?; sock.write_all(b"\n") .await .context("failed to write message to socket")?; Ok(()) } pub async fn recv( &mut self, ) -> anyhow::Result> { let Self(sock) = self; let mut buf = tokio::io::BufStream::new(sock); let mut line = String::new(); buf.read_line(&mut line) .await .context("failed to read message from socket")?; Ok(serde_json::from_str(&line) .map_err(|e| format!("failed to parse message '{line}': {e}"))) } } pub fn listen() -> anyhow::Result { let path = rbw::dirs::socket_file(); // if the socket already doesn't exist, that's fine let _ = std::fs::remove_file(&path); let sock = tokio::net::UnixListener::bind(&path) .context("failed to listen on socket")?; log::debug!("listening on socket {}", path.to_string_lossy()); Ok(sock) } rbw-1.13.2/src/bin/rbw-agent/timeout.rs000064400000000000000000000036641046102023000157740ustar 00000000000000use futures_util::StreamExt as _; #[derive(Debug, Hash, Eq, PartialEq, Copy, Clone)] enum Streams { Requests, Timer, } #[derive(Debug)] enum Action { Set(std::time::Duration), Clear, } pub struct Timeout { req_w: tokio::sync::mpsc::UnboundedSender, } impl Timeout { pub fn new() -> (Self, tokio::sync::mpsc::UnboundedReceiver<()>) { let (req_w, req_r) = tokio::sync::mpsc::unbounded_channel(); let (timer_w, timer_r) = tokio::sync::mpsc::unbounded_channel(); tokio::spawn(async move { enum Event { Request(Action), Timer, } let mut stream = tokio_stream::StreamMap::new(); stream.insert( Streams::Requests, tokio_stream::wrappers::UnboundedReceiverStream::new(req_r) .map(Event::Request) .boxed(), ); while let Some(event) = stream.next().await { match event { (_, Event::Request(Action::Set(dur))) => { stream.insert( Streams::Timer, futures_util::stream::once(tokio::time::sleep( dur, )) .map(|()| Event::Timer) .boxed(), ); } (_, Event::Request(Action::Clear)) => { stream.remove(&Streams::Timer); } (_, Event::Timer) => { timer_w.send(()).unwrap(); } } } }); (Self { req_w }, timer_r) } pub fn set(&self, dur: std::time::Duration) { self.req_w.send(Action::Set(dur)).unwrap(); } pub fn clear(&self) { self.req_w.send(Action::Clear).unwrap(); } } rbw-1.13.2/src/cipherstring.rs000064400000000000000000000231531046102023000143440ustar 00000000000000use crate::prelude::*; use aes::cipher::{ BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit as _, }; use hmac::Mac as _; use pkcs8::DecodePrivateKey as _; use rand::RngCore as _; use zeroize::Zeroize as _; pub enum CipherString { Symmetric { // ty: 2 (AES_256_CBC_HMAC_SHA256) iv: Vec, ciphertext: Vec, mac: Option>, }, Asymmetric { // ty: 4 (RSA_2048_OAEP_SHA1) ciphertext: Vec, }, } impl CipherString { pub fn new(s: &str) -> Result { let parts: Vec<&str> = s.split('.').collect(); if parts.len() != 2 { return Err(Error::InvalidCipherString { reason: "couldn't find type".to_string(), }); } let ty = parts[0].as_bytes(); if ty.len() != 1 { return Err(Error::UnimplementedCipherStringType { ty: parts[0].to_string(), }); } let ty = ty[0] - b'0'; let contents = parts[1]; match ty { 2 => { let parts: Vec<&str> = contents.split('|').collect(); if parts.len() < 2 || parts.len() > 3 { return Err(Error::InvalidCipherString { reason: format!( "type 2 cipherstring with {} parts", parts.len() ), }); } let iv = crate::base64::decode(parts[0]) .map_err(|source| Error::InvalidBase64 { source })?; let ciphertext = crate::base64::decode(parts[1]) .map_err(|source| Error::InvalidBase64 { source })?; let mac = if parts.len() > 2 { Some(crate::base64::decode(parts[2]).map_err( |source| Error::InvalidBase64 { source }, )?) } else { None }; Ok(Self::Symmetric { iv, ciphertext, mac, }) } 4 | 6 => { // the only difference between 4 and 6 is the HMAC256 // signature appended at the end // https://github.com/bitwarden/jslib/blob/785b681f61f81690de6df55159ab07ae710bcfad/src/enums/encryptionType.ts#L8 // format is: | let contents = contents.split('|').next().unwrap(); let ciphertext = crate::base64::decode(contents) .map_err(|source| Error::InvalidBase64 { source })?; Ok(Self::Asymmetric { ciphertext }) } _ => { if ty < 6 { Err(Error::TooOldCipherStringType { ty: ty.to_string() }) } else { Err(Error::UnimplementedCipherStringType { ty: ty.to_string(), }) } } } } pub fn encrypt_symmetric( keys: &crate::locked::Keys, plaintext: &[u8], ) -> Result { let iv = random_iv(); let cipher = cbc::Encryptor::::new( keys.enc_key().into(), iv.as_slice().into(), ); let ciphertext = cipher.encrypt_padded_vec_mut::(plaintext); let mut digest = hmac::Hmac::::new_from_slice(keys.mac_key()) .map_err(|source| Error::CreateHmac { source })?; digest.update(&iv); digest.update(&ciphertext); let mac = digest.finalize().into_bytes().as_slice().to_vec(); Ok(Self::Symmetric { iv, ciphertext, mac: Some(mac), }) } pub fn decrypt_symmetric( &self, keys: &crate::locked::Keys, entry_key: Option<&crate::locked::Keys>, ) -> Result> { if let Self::Symmetric { iv, ciphertext, mac, } = self { let cipher = decrypt_common_symmetric( entry_key.unwrap_or(keys), iv, ciphertext, mac.as_deref(), )?; cipher .decrypt_padded_vec_mut::(ciphertext) .map_err(|source| Error::Decrypt { source }) } else { Err(Error::InvalidCipherString { reason: "found an asymmetric cipherstring, expecting symmetric" .to_string(), }) } } pub fn decrypt_locked_symmetric( &self, keys: &crate::locked::Keys, ) -> Result { if let Self::Symmetric { iv, ciphertext, mac, } = self { let mut res = crate::locked::Vec::new(); res.extend(ciphertext.iter().copied()); let cipher = decrypt_common_symmetric( keys, iv, ciphertext, mac.as_deref(), )?; cipher .decrypt_padded_mut::(res.data_mut()) .map_err(|source| Error::Decrypt { source })?; Ok(res) } else { Err(Error::InvalidCipherString { reason: "found an asymmetric cipherstring, expecting symmetric" .to_string(), }) } } pub fn decrypt_locked_asymmetric( &self, private_key: &crate::locked::PrivateKey, ) -> Result { if let Self::Asymmetric { ciphertext } = self { let privkey_data = private_key.private_key(); let privkey_data = pkcs7_unpad(privkey_data).ok_or(Error::Padding)?; let pkey = rsa::RsaPrivateKey::from_pkcs8_der(privkey_data) .map_err(|source| Error::RsaPkcs8 { source })?; let mut bytes = pkey .decrypt(rsa::Oaep::new::(), ciphertext) .map_err(|source| Error::Rsa { source })?; // XXX it'd be great if the rsa crate would let us decrypt // into a preallocated buffer directly to avoid the // intermediate vec that needs to be manually zeroized, etc let mut res = crate::locked::Vec::new(); res.extend(bytes.iter().copied()); bytes.zeroize(); Ok(res) } else { Err(Error::InvalidCipherString { reason: "found a symmetric cipherstring, expecting asymmetric" .to_string(), }) } } } fn decrypt_common_symmetric( keys: &crate::locked::Keys, iv: &[u8], ciphertext: &[u8], mac: Option<&[u8]>, ) -> Result> { if let Some(mac) = mac { let mut key = hmac::Hmac::::new_from_slice(keys.mac_key()) .map_err(|source| Error::CreateHmac { source })?; key.update(iv); key.update(ciphertext); if key.verify(mac.into()).is_err() { return Err(Error::InvalidMac); } } cbc::Decryptor::::new_from_slices(keys.enc_key(), iv) .map_err(|source| Error::CreateBlockMode { source }) } impl std::fmt::Display for CipherString { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Symmetric { iv, ciphertext, mac, } => { let iv = crate::base64::encode(iv); let ciphertext = crate::base64::encode(ciphertext); if let Some(mac) = &mac { let mac = crate::base64::encode(mac); write!(f, "2.{iv}|{ciphertext}|{mac}") } else { write!(f, "2.{iv}|{ciphertext}") } } Self::Asymmetric { ciphertext } => { let ciphertext = crate::base64::encode(ciphertext); write!(f, "4.{ciphertext}") } } } } fn random_iv() -> Vec { let mut iv = vec![0_u8; 16]; let mut rng = rand::thread_rng(); rng.fill_bytes(&mut iv); iv } // XXX this should ideally just be block_padding::Pkcs7::unpad, but i can't // figure out how to get the generic types to work out fn pkcs7_unpad(b: &[u8]) -> Option<&[u8]> { if b.is_empty() { return None; } let padding_val = b[b.len() - 1]; if padding_val == 0 { return None; } let padding_len = usize::from(padding_val); if padding_len > b.len() { return None; } for c in b.iter().copied().skip(b.len() - padding_len) { if c != padding_val { return None; } } Some(&b[..b.len() - padding_len]) } #[test] fn test_pkcs7_unpad() { let tests = [ (&[][..], None), (&[0x01][..], Some(&[][..])), (&[0x02, 0x02][..], Some(&[][..])), (&[0x03, 0x03, 0x03][..], Some(&[][..])), (&[0x69, 0x01][..], Some(&[0x69][..])), (&[0x69, 0x02, 0x02][..], Some(&[0x69][..])), (&[0x69, 0x03, 0x03, 0x03][..], Some(&[0x69][..])), (&[0x02][..], None), (&[0x03][..], None), (&[0x69, 0x69, 0x03, 0x03][..], None), (&[0x00][..], None), (&[0x02, 0x00][..], None), ]; for (input, expected) in tests { let got = pkcs7_unpad(input); assert_eq!(got, expected); } } rbw-1.13.2/src/config.rs000064400000000000000000000163361046102023000131150ustar 00000000000000use crate::prelude::*; use std::io::{Read as _, Write as _}; use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; #[derive(serde::Serialize, serde::Deserialize, Debug)] pub struct Config { pub email: Option, pub sso_id: Option, pub base_url: Option, pub identity_url: Option, pub ui_url: Option, pub notifications_url: Option, #[serde(default = "default_lock_timeout")] pub lock_timeout: u64, #[serde(default = "default_sync_interval")] pub sync_interval: u64, #[serde(default = "default_pinentry")] pub pinentry: String, pub client_cert_path: Option, // backcompat, no longer generated in new configs #[serde(skip_serializing)] pub device_id: Option, } impl Default for Config { fn default() -> Self { Self { email: None, sso_id: None, base_url: None, identity_url: None, ui_url: None, notifications_url: None, lock_timeout: default_lock_timeout(), sync_interval: default_sync_interval(), pinentry: default_pinentry(), client_cert_path: None, device_id: None, } } } pub fn default_lock_timeout() -> u64 { 3600 } pub fn default_sync_interval() -> u64 { 3600 } pub fn default_pinentry() -> String { "pinentry".to_string() } impl Config { pub fn new() -> Self { Self::default() } pub fn load() -> Result { let file = crate::dirs::config_file(); let mut fh = std::fs::File::open(&file).map_err(|source| { Error::LoadConfig { source, file: file.clone(), } })?; let mut json = String::new(); fh.read_to_string(&mut json) .map_err(|source| Error::LoadConfig { source, file: file.clone(), })?; let mut slf: Self = serde_json::from_str(&json) .map_err(|source| Error::LoadConfigJson { source, file })?; if slf.lock_timeout == 0 { log::warn!("lock_timeout must be greater than 0"); slf.lock_timeout = default_lock_timeout(); } Ok(slf) } pub async fn load_async() -> Result { let file = crate::dirs::config_file(); let mut fh = tokio::fs::File::open(&file).await.map_err(|source| { Error::LoadConfigAsync { source, file: file.clone(), } })?; let mut json = String::new(); fh.read_to_string(&mut json).await.map_err(|source| { Error::LoadConfigAsync { source, file: file.clone(), } })?; let mut slf: Self = serde_json::from_str(&json) .map_err(|source| Error::LoadConfigJson { source, file })?; if slf.lock_timeout == 0 { log::warn!("lock_timeout must be greater than 0"); slf.lock_timeout = default_lock_timeout(); } Ok(slf) } pub fn save(&self) -> Result<()> { let file = crate::dirs::config_file(); // unwrap is safe here because Self::filename is explicitly // constructed as a filename in a directory std::fs::create_dir_all(file.parent().unwrap()).map_err( |source| Error::SaveConfig { source, file: file.clone(), }, )?; let mut fh = std::fs::File::create(&file).map_err(|source| { Error::SaveConfig { source, file: file.clone(), } })?; fh.write_all( serde_json::to_string(self) .map_err(|source| Error::SaveConfigJson { source, file: file.clone(), })? .as_bytes(), ) .map_err(|source| Error::SaveConfig { source, file })?; Ok(()) } pub fn validate() -> Result<()> { let config = Self::load()?; if config.email.is_none() { return Err(Error::ConfigMissingEmail); } Ok(()) } pub fn base_url(&self) -> String { self.base_url.clone().map_or_else( || "https://api.bitwarden.com".to_string(), |url| { let clean_url = url.trim_end_matches('/').to_string(); if clean_url == "https://api.bitwarden.eu" { clean_url } else { format!("{clean_url}/api") } }, ) } pub fn identity_url(&self) -> String { self.identity_url.clone().unwrap_or_else(|| { self.base_url.clone().map_or_else( || "https://identity.bitwarden.com".to_string(), |url| { let clean_url = url.trim_end_matches('/').to_string(); if clean_url == "https://identity.bitwarden.eu" { clean_url } else { format!("{clean_url}/identity") } }, ) }) } pub fn ui_url(&self) -> String { // TODO: default to either vault.bitwarden.com or vault.bitwarden.eu based on the base_url? self.ui_url .clone() .unwrap_or_else(|| "https://vault.bitwarden.com".to_string()) } pub fn notifications_url(&self) -> String { self.notifications_url.clone().unwrap_or_else(|| { self.base_url.clone().map_or_else( || "https://notifications.bitwarden.com".to_string(), |url| { let clean_url = url.trim_end_matches('/').to_string(); if clean_url == "https://notifications.bitwarden.eu" { clean_url } else { format!("{clean_url}/notifications") } }, ) }) } pub fn client_cert_path(&self) -> Option<&std::path::Path> { self.client_cert_path.as_deref() } pub fn server_name(&self) -> String { self.base_url .clone() .unwrap_or_else(|| "default".to_string()) } } pub async fn device_id(config: &Config) -> Result { let file = crate::dirs::device_id_file(); if let Ok(mut fh) = tokio::fs::File::open(&file).await { let mut s = String::new(); fh.read_to_string(&mut s) .await .map_err(|e| Error::LoadDeviceId { source: e, file: file.clone(), })?; Ok(s.trim().to_string()) } else { let id = config.device_id.as_ref().map_or_else( || uuid::Uuid::new_v4().hyphenated().to_string(), String::to_string, ); let mut fh = tokio::fs::File::create(&file).await.map_err(|e| { Error::LoadDeviceId { source: e, file: file.clone(), } })?; fh.write_all(id.as_bytes()).await.map_err(|e| { Error::LoadDeviceId { source: e, file: file.clone(), } })?; Ok(id) } } rbw-1.13.2/src/db.rs000064400000000000000000000222411046102023000122250ustar 00000000000000use crate::prelude::*; use std::io::{Read as _, Write as _}; use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub struct Entry { pub id: String, pub org_id: Option, pub folder: Option, pub folder_id: Option, pub name: String, pub data: EntryData, pub fields: Vec, pub notes: Option, pub history: Vec, pub key: Option, } #[derive(serde::Serialize, Debug, Clone, Eq, PartialEq)] pub struct Uri { pub uri: String, pub match_type: Option, } // backwards compatibility impl<'de> serde::Deserialize<'de> for Uri { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { struct StringOrUri; impl<'de> serde::de::Visitor<'de> for StringOrUri { type Value = Uri; fn expecting( &self, formatter: &mut std::fmt::Formatter, ) -> std::fmt::Result { formatter.write_str("uri") } fn visit_str( self, value: &str, ) -> std::result::Result where E: serde::de::Error, { Ok(Uri { uri: value.to_string(), match_type: None, }) } fn visit_map( self, mut map: M, ) -> std::result::Result where M: serde::de::MapAccess<'de>, { let mut uri = None; let mut match_type = None; while let Some(key) = map.next_key()? { match key { "uri" => { if uri.is_some() { return Err( serde::de::Error::duplicate_field("uri"), ); } uri = Some(map.next_value()?); } "match_type" => { if match_type.is_some() { return Err( serde::de::Error::duplicate_field( "match_type", ), ); } match_type = map.next_value()?; } _ => { return Err(serde::de::Error::unknown_field( key, &["uri", "match_type"], )) } } } uri.map_or_else( || Err(serde::de::Error::missing_field("uri")), |uri| Ok(Self::Value { uri, match_type }), ) } } deserializer.deserialize_any(StringOrUri) } } #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub enum EntryData { Login { username: Option, password: Option, totp: Option, uris: Vec, }, Card { cardholder_name: Option, number: Option, brand: Option, exp_month: Option, exp_year: Option, code: Option, }, Identity { title: Option, first_name: Option, middle_name: Option, last_name: Option, address1: Option, address2: Option, address3: Option, city: Option, state: Option, postal_code: Option, country: Option, phone: Option, email: Option, ssn: Option, license_number: Option, passport_number: Option, username: Option, }, SecureNote, } #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub struct Field { pub ty: Option, pub name: Option, pub value: Option, pub linked_id: Option, } #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub struct HistoryEntry { pub last_used_date: String, pub password: String, } #[derive(serde::Serialize, serde::Deserialize, Default, Debug)] pub struct Db { pub access_token: Option, pub refresh_token: Option, pub kdf: Option, pub iterations: Option, pub memory: Option, pub parallelism: Option, pub protected_key: Option, pub protected_private_key: Option, pub protected_org_keys: std::collections::HashMap, pub entries: Vec, } impl Db { pub fn new() -> Self { Self::default() } pub fn load(server: &str, email: &str) -> Result { let file = crate::dirs::db_file(server, email); let mut fh = std::fs::File::open(&file).map_err(|source| Error::LoadDb { source, file: file.clone(), })?; let mut json = String::new(); fh.read_to_string(&mut json) .map_err(|source| Error::LoadDb { source, file: file.clone(), })?; let slf: Self = serde_json::from_str(&json) .map_err(|source| Error::LoadDbJson { source, file })?; Ok(slf) } pub async fn load_async(server: &str, email: &str) -> Result { let file = crate::dirs::db_file(server, email); let mut fh = tokio::fs::File::open(&file).await.map_err(|source| { Error::LoadDbAsync { source, file: file.clone(), } })?; let mut json = String::new(); fh.read_to_string(&mut json).await.map_err(|source| { Error::LoadDbAsync { source, file: file.clone(), } })?; let slf: Self = serde_json::from_str(&json) .map_err(|source| Error::LoadDbJson { source, file })?; Ok(slf) } // XXX need to make this atomic pub fn save(&self, server: &str, email: &str) -> Result<()> { let file = crate::dirs::db_file(server, email); // unwrap is safe here because Self::filename is explicitly // constructed as a filename in a directory std::fs::create_dir_all(file.parent().unwrap()).map_err( |source| Error::SaveDb { source, file: file.clone(), }, )?; let mut fh = std::fs::File::create(&file).map_err(|source| Error::SaveDb { source, file: file.clone(), })?; fh.write_all( serde_json::to_string(self) .map_err(|source| Error::SaveDbJson { source, file: file.clone(), })? .as_bytes(), ) .map_err(|source| Error::SaveDb { source, file })?; Ok(()) } // XXX need to make this atomic pub async fn save_async(&self, server: &str, email: &str) -> Result<()> { let file = crate::dirs::db_file(server, email); // unwrap is safe here because Self::filename is explicitly // constructed as a filename in a directory tokio::fs::create_dir_all(file.parent().unwrap()) .await .map_err(|source| Error::SaveDbAsync { source, file: file.clone(), })?; let mut fh = tokio::fs::File::create(&file).await.map_err(|source| { Error::SaveDbAsync { source, file: file.clone(), } })?; fh.write_all( serde_json::to_string(self) .map_err(|source| Error::SaveDbJson { source, file: file.clone(), })? .as_bytes(), ) .await .map_err(|source| Error::SaveDbAsync { source, file })?; Ok(()) } pub fn remove(server: &str, email: &str) -> Result<()> { let file = crate::dirs::db_file(server, email); let res = std::fs::remove_file(&file); if let Err(e) = &res { if e.kind() == std::io::ErrorKind::NotFound { return Ok(()); } } res.map_err(|source| Error::RemoveDb { source, file })?; Ok(()) } pub fn needs_login(&self) -> bool { self.access_token.is_none() || self.refresh_token.is_none() || self.iterations.is_none() || self.kdf.is_none() || self.protected_key.is_none() } } rbw-1.13.2/src/dirs.rs000064400000000000000000000056101046102023000126020ustar 00000000000000use crate::prelude::*; use std::os::unix::fs::PermissionsExt as _; pub fn make_all() -> Result<()> { let cache_dir = cache_dir(); std::fs::create_dir_all(&cache_dir).map_err(|source| { Error::CreateDirectory { source, file: cache_dir, } })?; let runtime_dir = runtime_dir(); std::fs::create_dir_all(&runtime_dir).map_err(|source| { Error::CreateDirectory { source, file: runtime_dir.clone(), } })?; std::fs::set_permissions( &runtime_dir, std::fs::Permissions::from_mode(0o700), ) .map_err(|source| Error::CreateDirectory { source, file: runtime_dir, })?; let data_dir = data_dir(); std::fs::create_dir_all(&data_dir).map_err(|source| { Error::CreateDirectory { source, file: data_dir, } })?; Ok(()) } pub fn config_file() -> std::path::PathBuf { config_dir().join("config.json") } const INVALID_PATH: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS.add(b'/').add(b'%').add(b':'); pub fn db_file(server: &str, email: &str) -> std::path::PathBuf { let server = percent_encoding::percent_encode(server.as_bytes(), INVALID_PATH) .to_string(); cache_dir().join(format!("{server}:{email}.json")) } pub fn pid_file() -> std::path::PathBuf { runtime_dir().join("pidfile") } pub fn agent_stdout_file() -> std::path::PathBuf { data_dir().join("agent.out") } pub fn agent_stderr_file() -> std::path::PathBuf { data_dir().join("agent.err") } pub fn device_id_file() -> std::path::PathBuf { data_dir().join("device_id") } pub fn socket_file() -> std::path::PathBuf { runtime_dir().join("socket") } fn config_dir() -> std::path::PathBuf { let project_dirs = directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.config_dir().to_path_buf() } fn cache_dir() -> std::path::PathBuf { let project_dirs = directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.cache_dir().to_path_buf() } fn data_dir() -> std::path::PathBuf { let project_dirs = directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.data_dir().to_path_buf() } fn runtime_dir() -> std::path::PathBuf { let project_dirs = directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.runtime_dir().map_or_else( || { format!( "{}/{}-{}", std::env::temp_dir().to_string_lossy(), &profile(), rustix::process::getuid().as_raw() ) .into() }, std::path::Path::to_path_buf, ) } pub fn profile() -> String { match std::env::var("RBW_PROFILE") { Ok(profile) if !profile.is_empty() => format!("rbw-{profile}"), _ => "rbw".to_string(), } } rbw-1.13.2/src/edit.rs000064400000000000000000000057131046102023000125720ustar 00000000000000use crate::prelude::*; use std::io::{Read as _, Write as _}; use is_terminal::IsTerminal as _; pub fn edit(contents: &str, help: &str) -> Result { if !std::io::stdin().is_terminal() { // directly read from piped content return match std::io::read_to_string(std::io::stdin()) { Err(e) => Err(Error::FailedToReadFromStdin { err: e }), Ok(res) => Ok(res), }; } let mut var = "VISUAL"; let editor = std::env::var_os(var).unwrap_or_else(|| { var = "EDITOR"; std::env::var_os(var).unwrap_or_else(|| "/usr/bin/vim".into()) }); let dir = tempfile::tempdir().unwrap(); let file = dir.path().join("rbw"); let mut fh = std::fs::File::create(&file).unwrap(); fh.write_all(contents.as_bytes()).unwrap(); fh.write_all(help.as_bytes()).unwrap(); drop(fh); let (cmd, args) = if contains_shell_metacharacters(&editor) { let mut cmdline = std::ffi::OsString::new(); cmdline.extend([ editor.as_ref(), std::ffi::OsStr::new(" "), file.as_os_str(), ]); let editor_args = vec![std::ffi::OsString::from("-c"), cmdline]; (std::path::Path::new("/bin/sh"), editor_args) } else { let editor = std::path::Path::new(&editor); let mut editor_args = vec![]; #[allow(clippy::single_match_else)] // more to come match editor.file_name() { Some(editor) => match editor.to_str() { Some("vim" | "nvim") => { // disable swap files and viminfo for password entry editor_args.push(std::ffi::OsString::from("-ni")); editor_args.push(std::ffi::OsString::from("NONE")); } _ => { // other editor support welcomed } }, None => { return Err(Error::InvalidEditor { var: var.to_string(), editor: editor.as_os_str().to_os_string(), }) } } editor_args.push(file.clone().into_os_string()); (editor, editor_args) }; let res = std::process::Command::new(cmd).args(&args).status(); match res { Ok(res) => { if !res.success() { return Err(Error::FailedToRunEditor { editor: cmd.to_owned(), args, res, }); } } Err(err) => { return Err(Error::FailedToFindEditor { editor: cmd.to_owned(), err, }) } } let mut fh = std::fs::File::open(&file).unwrap(); let mut contents = String::new(); fh.read_to_string(&mut contents).unwrap(); drop(fh); Ok(contents) } fn contains_shell_metacharacters(cmd: &std::ffi::OsStr) -> bool { cmd.to_str() .map_or(false, |s| s.contains(&[' ', '$', '\'', '"'][..])) } rbw-1.13.2/src/error.rs000064400000000000000000000151401046102023000127710ustar 00000000000000#[derive(thiserror::Error, Debug)] pub enum Error { #[error("email address not set")] ConfigMissingEmail, #[error("failed to create block mode decryptor")] CreateBlockMode { source: aes::cipher::InvalidLength }, #[error("failed to create block mode decryptor")] CreateHmac { source: aes::cipher::InvalidLength }, #[error("failed to create directory at {}", .file.display())] CreateDirectory { source: std::io::Error, file: std::path::PathBuf, }, #[error("failed to create reqwest client")] CreateReqwestClient { source: reqwest::Error }, #[error("failed to create sso callback server: {err}")] CreateSSOCallbackServer { err: std::io::Error }, #[error("failed to decrypt")] Decrypt { source: block_padding::UnpadError }, #[error("failed to find free port in {range}")] FailedToFindFreePort { range: String }, #[error("failed to parse pinentry output ({out:?})")] FailedToParsePinentry { out: String }, #[error("failed to process sso callback ({msg})")] FailedToProcessSSOCallback { msg: String }, #[error("failed to open web browser: {err}")] FailedToOpenWebBrowser { err: std::io::Error }, #[error("failed to read from stdin: {err}")] FailedToReadFromStdin { err: std::io::Error }, #[error( "failed to run editor {}: {err}", .editor.to_string_lossy(), )] FailedToFindEditor { editor: std::path::PathBuf, err: std::io::Error, }, #[error( "failed to run editor {} {}: {res:?}", .editor.to_string_lossy(), .args.iter().map(|s| s.to_string_lossy()).collect::>().join(" ") )] FailedToRunEditor { editor: std::path::PathBuf, args: Vec, res: std::process::ExitStatus, }, #[error("failed to expand with hkdf")] HkdfExpand, #[error("incorrect api key")] IncorrectApiKey, #[error("{message}")] IncorrectPassword { message: String }, #[error("invalid base64")] InvalidBase64 { source: base64::DecodeError }, #[error("invalid cipherstring: {reason}")] InvalidCipherString { reason: String }, #[error( "invalid value for ${var}: {}", .editor.to_string_lossy() )] InvalidEditor { var: String, editor: std::ffi::OsString, }, #[error("invalid mac")] InvalidMac, #[error("invalid two factor provider type: {ty}")] InvalidTwoFactorProvider { ty: String }, #[error("failed to parse JSON")] Json { source: serde_path_to_error::Error, }, #[error("failed to load config from {}", .file.display())] LoadConfig { source: std::io::Error, file: std::path::PathBuf, }, #[error("failed to load config from {}", .file.display())] LoadConfigAsync { source: tokio::io::Error, file: std::path::PathBuf, }, #[error("failed to load config from {}", .file.display())] LoadConfigJson { source: serde_json::Error, file: std::path::PathBuf, }, #[error("failed to load db from {}", .file.display())] LoadDb { source: std::io::Error, file: std::path::PathBuf, }, #[error("failed to load db from {}", .file.display())] LoadDbAsync { source: tokio::io::Error, file: std::path::PathBuf, }, #[error("failed to load db from {}", .file.display())] LoadDbJson { source: serde_json::Error, file: std::path::PathBuf, }, #[error("failed to load device id from {}", .file.display())] LoadDeviceId { source: tokio::io::Error, file: std::path::PathBuf, }, #[error("failed to load client cert from {}", .file.display())] LoadClientCert { source: tokio::io::Error, file: std::path::PathBuf, }, #[error("invalid padding")] Padding, #[error("failed to parse match type {s}")] ParseMatchType { s: String }, #[error("pbkdf2 requires at least 1 iteration (got 0)")] Pbkdf2ZeroIterations, #[error("failed to run pbkdf2")] Pbkdf2, #[error("failed to run argon2")] Argon2, #[error("pinentry cancelled")] PinentryCancelled, #[error("pinentry error: {error}")] PinentryErrorMessage { error: String }, #[error("error reading pinentry output")] PinentryReadOutput { source: tokio::io::Error }, #[error("error waiting for pinentry to exit")] PinentryWait { source: tokio::io::Error }, #[error("This device has not yet been registered with the Bitwarden server. Run `rbw register` first, and then try again.")] RegistrationRequired, #[error("failed to remove db at {}", .file.display())] RemoveDb { source: std::io::Error, file: std::path::PathBuf, }, #[error("api request returned error: {status}")] RequestFailed { status: u16 }, #[error("api request unauthorized")] RequestUnauthorized, #[error("error making api request")] Reqwest { source: reqwest::Error }, #[error("failed to decrypt")] Rsa { source: rsa::errors::Error }, #[error("failed to decrypt")] RsaPkcs8 { source: rsa::pkcs8::Error }, #[error("failed to save config to {}", .file.display())] SaveConfig { source: std::io::Error, file: std::path::PathBuf, }, #[error("failed to save config to {}", .file.display())] SaveConfigJson { source: serde_json::Error, file: std::path::PathBuf, }, #[error("failed to save db to {}", .file.display())] SaveDb { source: std::io::Error, file: std::path::PathBuf, }, #[error("failed to save db to {}", .file.display())] SaveDbAsync { source: tokio::io::Error, file: std::path::PathBuf, }, #[error("failed to save db to {}", .file.display())] SaveDbJson { source: serde_json::Error, file: std::path::PathBuf, }, #[error("error spawning pinentry")] Spawn { source: tokio::io::Error }, #[error("cipherstring type {ty} too old\n\nPlease rotate your account encryption key (https://bitwarden.com/help/article/account-encryption-key/) and try again.")] TooOldCipherStringType { ty: String }, #[error("two factor required")] TwoFactorRequired { providers: Vec, }, #[error("unimplemented cipherstring type: {ty}")] UnimplementedCipherStringType { ty: String }, #[error("error writing to pinentry stdin")] WriteStdin { source: tokio::io::Error }, #[error("invalid kdf type: {ty}")] InvalidKdfType { ty: String }, } pub type Result = std::result::Result; rbw-1.13.2/src/identity.rs000064400000000000000000000055141046102023000134750ustar 00000000000000use crate::prelude::*; use sha1::Digest as _; pub struct Identity { pub email: String, pub keys: crate::locked::Keys, pub master_password_hash: crate::locked::PasswordHash, } impl Identity { pub fn new( email: &str, password: &crate::locked::Password, kdf: crate::api::KdfType, iterations: u32, memory: Option, parallelism: Option, ) -> Result { let email = email.trim().to_lowercase(); let iterations = std::num::NonZeroU32::new(iterations) .ok_or(Error::Pbkdf2ZeroIterations)?; let mut keys = crate::locked::Vec::new(); keys.extend(std::iter::repeat(0).take(64)); let enc_key = &mut keys.data_mut()[0..32]; match kdf { crate::api::KdfType::Pbkdf2 => { pbkdf2::pbkdf2::>( password.password(), email.as_bytes(), iterations.get(), enc_key, ) .map_err(|_| Error::Pbkdf2)?; } crate::api::KdfType::Argon2id => { let mut hasher = sha2::Sha256::new(); hasher.update(email.as_bytes()); let salt = hasher.finalize(); let argon2_config = argon2::Argon2::new( argon2::Algorithm::Argon2id, argon2::Version::V0x13, argon2::Params::new( memory.unwrap() * 1024, iterations.get(), parallelism.unwrap(), Some(32), ) .unwrap(), ); argon2::Argon2::hash_password_into( &argon2_config, password.password(), &salt, enc_key, ) .map_err(|_| Error::Argon2)?; } }; let mut hash = crate::locked::Vec::new(); hash.extend(std::iter::repeat(0).take(32)); pbkdf2::pbkdf2::>( enc_key, password.password(), 1, hash.data_mut(), ) .map_err(|_| Error::Pbkdf2)?; let hkdf = hkdf::Hkdf::::from_prk(enc_key) .map_err(|_| Error::HkdfExpand)?; hkdf.expand(b"enc", enc_key) .map_err(|_| Error::HkdfExpand)?; let mac_key = &mut keys.data_mut()[32..64]; hkdf.expand(b"mac", mac_key) .map_err(|_| Error::HkdfExpand)?; let keys = crate::locked::Keys::new(keys); let master_password_hash = crate::locked::PasswordHash::new(hash); Ok(Self { email: email.to_string(), keys, master_password_hash, }) } } rbw-1.13.2/src/json.rs000064400000000000000000000026601046102023000126140ustar 00000000000000use crate::prelude::*; pub trait DeserializeJsonWithPath { fn json_with_path(self) -> Result; } impl DeserializeJsonWithPath for String { fn json_with_path(self) -> Result { let jd = &mut serde_json::Deserializer::from_str(&self); serde_path_to_error::deserialize(jd) .map_err(|source| Error::Json { source }) } } impl DeserializeJsonWithPath for reqwest::blocking::Response { fn json_with_path(self) -> Result { let bytes = self.bytes().map_err(|source| Error::Reqwest { source })?; let jd = &mut serde_json::Deserializer::from_slice(&bytes); serde_path_to_error::deserialize(jd) .map_err(|source| Error::Json { source }) } } pub trait DeserializeJsonWithPathAsync { #[allow(async_fn_in_trait)] async fn json_with_path( self, ) -> Result; } impl DeserializeJsonWithPathAsync for reqwest::Response { async fn json_with_path( self, ) -> Result { let bytes = self .bytes() .await .map_err(|source| Error::Reqwest { source })?; let jd = &mut serde_json::Deserializer::from_slice(&bytes); serde_path_to_error::deserialize(jd) .map_err(|source| Error::Json { source }) } } rbw-1.13.2/src/lib.rs000064400000000000000000000004151046102023000124050ustar 00000000000000pub mod actions; pub mod api; pub mod base64; pub mod cipherstring; pub mod config; pub mod db; pub mod dirs; pub mod edit; pub mod error; pub mod identity; pub mod json; pub mod locked; pub mod pinentry; mod prelude; pub mod protocol; pub mod pwgen; pub mod wordlist; rbw-1.13.2/src/locked.rs000064400000000000000000000063201046102023000131010ustar 00000000000000use zeroize::Zeroize as _; const LEN: usize = 4096; static REGION_LOCK_WORKS: std::sync::OnceLock = std::sync::OnceLock::new(); pub struct Vec { data: Box>, _lock: Option, } impl Default for Vec { fn default() -> Self { let data = Box::new(arrayvec::ArrayVec::<_, LEN>::new()); let lock = match REGION_LOCK_WORKS.get() { Some(true) => { Some(region::lock(data.as_ptr(), data.capacity()).unwrap()) } Some(false) => None, None => match region::lock(data.as_ptr(), data.capacity()) { Ok(lock) => { let _ = REGION_LOCK_WORKS.set(true); Some(lock) } Err(e) => { if REGION_LOCK_WORKS.set(false).is_ok() { eprintln!("failed to lock memory region: {e}"); } None } }, }; Self { data, _lock: lock } } } impl Vec { pub fn new() -> Self { Self::default() } pub fn data(&self) -> &[u8] { self.data.as_slice() } pub fn data_mut(&mut self) -> &mut [u8] { self.data.as_mut_slice() } pub fn zero(&mut self) { self.truncate(0); self.data.extend(std::iter::repeat(0).take(LEN)); } pub fn extend(&mut self, it: impl Iterator) { self.data.extend(it); } pub fn truncate(&mut self, len: usize) { self.data.truncate(len); } } impl Drop for Vec { fn drop(&mut self) { self.zero(); self.data.as_mut().zeroize(); } } impl Clone for Vec { fn clone(&self) -> Self { let mut new_vec = Self::new(); new_vec.extend(self.data().iter().copied()); new_vec } } #[derive(Clone)] pub struct Password { password: Vec, } impl Password { pub fn new(password: Vec) -> Self { Self { password } } pub fn password(&self) -> &[u8] { self.password.data() } } #[derive(Clone)] pub struct Keys { keys: Vec, } impl Keys { pub fn new(keys: Vec) -> Self { Self { keys } } pub fn enc_key(&self) -> &[u8] { &self.keys.data()[0..32] } pub fn mac_key(&self) -> &[u8] { &self.keys.data()[32..64] } } #[derive(Clone)] pub struct PasswordHash { hash: Vec, } impl PasswordHash { pub fn new(hash: Vec) -> Self { Self { hash } } pub fn hash(&self) -> &[u8] { self.hash.data() } } #[derive(Clone)] pub struct PrivateKey { private_key: Vec, } impl PrivateKey { pub fn new(private_key: Vec) -> Self { Self { private_key } } pub fn private_key(&self) -> &[u8] { self.private_key.data() } } #[derive(Clone)] pub struct ApiKey { client_id: Password, client_secret: Password, } impl ApiKey { pub fn new(client_id: Password, client_secret: Password) -> Self { Self { client_id, client_secret, } } pub fn client_id(&self) -> &[u8] { self.client_id.password() } pub fn client_secret(&self) -> &[u8] { self.client_secret.password() } } rbw-1.13.2/src/pinentry.rs000064400000000000000000000175351046102023000135220ustar 00000000000000use crate::prelude::*; use std::convert::TryFrom as _; use tokio::io::AsyncWriteExt as _; pub async fn getpin( pinentry: &str, prompt: &str, desc: &str, err: Option<&str>, environment: &crate::protocol::Environment, grab: bool, ) -> Result { let mut opts = tokio::process::Command::new(pinentry); opts.stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()); let mut args = vec!["--timeout".into(), "0".into()]; if let Some(tty) = environment.tty() { args.extend(["--ttyname".into(), tty.into()]); } let env_vars = environment.env_vars(); // Not all pinentry appear to respect the --display flag, so we also keep the environment // variable. if let Some(display) = env_vars.get(std::ffi::OsString::from("DISPLAY").as_os_str()) { args.extend(["--display".into(), display.clone()]); } if !grab { args.push("--no-global-grab".into()); } opts.args(args); for env_var in &*crate::protocol::ENVIRONMENT_VARIABLES_OS { if let Some(val) = env_vars.get(env_var) { opts.env(env_var, val); } else { opts.env_remove(env_var); } } opts.envs(env_vars); let mut child = opts.spawn().map_err(|source| Error::Spawn { source })?; // unwrap is safe because we specified stdin as piped in the command opts // above let mut stdin = child.stdin.take().unwrap(); let mut ncommands = 1; stdin .write_all(b"SETTITLE rbw\n") .await .map_err(|source| Error::WriteStdin { source })?; ncommands += 1; stdin .write_all(format!("SETPROMPT {prompt}\n").as_bytes()) .await .map_err(|source| Error::WriteStdin { source })?; ncommands += 1; stdin .write_all(format!("SETDESC {desc}\n").as_bytes()) .await .map_err(|source| Error::WriteStdin { source })?; ncommands += 1; if let Some(err) = err { stdin .write_all(format!("SETERROR {err}\n").as_bytes()) .await .map_err(|source| Error::WriteStdin { source })?; ncommands += 1; } stdin .write_all(b"GETPIN\n") .await .map_err(|source| Error::WriteStdin { source })?; ncommands += 1; drop(stdin); let mut buf = crate::locked::Vec::new(); buf.zero(); // unwrap is safe because we specified stdout as piped in the command opts // above let len = read_password( ncommands, buf.data_mut(), child.stdout.as_mut().unwrap(), ) .await?; buf.truncate(len); child .wait() .await .map_err(|source| Error::PinentryWait { source })?; Ok(crate::locked::Password::new(buf)) } async fn read_password( mut ncommands: u8, data: &mut [u8], mut r: R, ) -> Result where R: tokio::io::AsyncRead + tokio::io::AsyncReadExt + Unpin + Send, { let mut len = 0; loop { let nl = data.iter().take(len).position(|c| *c == b'\n'); if let Some(nl) = nl { if data.starts_with(b"OK") { if ncommands == 1 { len = 0; break; } data.copy_within((nl + 1).., 0); len -= nl + 1; ncommands -= 1; } else if data.starts_with(b"D ") { data.copy_within(2..nl, 0); len = nl - 2; break; } else if data.starts_with(b"ERR ") { let line: Vec = data.iter().take(nl).copied().collect(); let line = String::from_utf8(line).unwrap(); let mut split = line.splitn(3, ' '); let _ = split.next(); // ERR let code = split.next(); match code { Some("83886179") => { return Err(Error::PinentryCancelled); } Some(code) => { if let Some(error) = split.next() { return Err(Error::PinentryErrorMessage { error: error.to_string(), }); } return Err(Error::PinentryErrorMessage { error: format!("unknown error ({code})"), }); } None => { return Err(Error::PinentryErrorMessage { error: "unknown error".to_string(), }); } } } else { return Err(Error::FailedToParsePinentry { out: String::from_utf8_lossy(data).to_string(), }); } } else { let bytes = r .read(&mut data[len..]) .await .map_err(|source| Error::PinentryReadOutput { source })?; if bytes == 0 { return Err(Error::PinentryReadOutput { source: std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "unexpected EOF", ), }); } len += bytes; } } len = percent_decode(&mut data[..len]); Ok(len) } // not using the percent-encoding crate because it doesn't provide a way to do // this in-place, and we want the password to always live within the locked // vec. should really move something like this into the percent-encoding crate // at some point. fn percent_decode(buf: &mut [u8]) -> usize { let mut read_idx = 0; let mut write_idx = 0; let len = buf.len(); while read_idx < len { let mut c = buf[read_idx]; if c == b'%' && read_idx + 2 < len { if let Some(h) = char::from(buf[read_idx + 1]).to_digit(16) { if let Some(l) = char::from(buf[read_idx + 2]).to_digit(16) { // h and l were parsed from a single hex digit, so they // must be in the range 0-15, so these unwraps are safe c = u8::try_from(h).unwrap() * 0x10 + u8::try_from(l).unwrap(); read_idx += 2; } } } buf[write_idx] = c; read_idx += 1; write_idx += 1; } write_idx } #[test] fn test_read_password() { let good_inputs = &[ (0, &b"D super secret password\n"[..]), (4, &b"OK\nOK\nOK\nD super secret password\nOK\n"[..]), (12, &b"OK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nD super secret password\nOK\n"[..]), (24, &b"OK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nOK\nD super secret password\nOK\n"[..]), ]; for (ncommands, input) in good_inputs { let mut buf = [0; 64]; tokio::runtime::Runtime::new().unwrap().block_on(async { let len = read_password(*ncommands, &mut buf, &input[..]) .await .unwrap(); assert_eq!(&buf[0..len], b"super secret password"); }); } let match_inputs = &[ (&b"OK\nOK\nOK\nOK\n"[..], &b""[..]), (&b"D foo%25bar\n"[..], &b"foo%bar"[..]), (&b"D foo%0abar\n"[..], &b"foo\nbar"[..]), (&b"D foo%0Abar\n"[..], &b"foo\nbar"[..]), (&b"D foo%0Gbar\n"[..], &b"foo%0Gbar"[..]), (&b"D foo%0\n"[..], &b"foo%0"[..]), (&b"D foo%\n"[..], &b"foo%"[..]), (&b"D %25foo\n"[..], &b"%foo"[..]), (&b"D %25\n"[..], &b"%"[..]), ]; for (input, output) in match_inputs { let mut buf = [0; 64]; tokio::runtime::Runtime::new().unwrap().block_on(async { let len = read_password(4, &mut buf, &input[..]).await.unwrap(); assert_eq!(&buf[0..len], &output[..]); }); } } rbw-1.13.2/src/prelude.rs000064400000000000000000000000471046102023000133000ustar 00000000000000pub use crate::error::{Error, Result}; rbw-1.13.2/src/protocol.rs000064400000000000000000000127101046102023000135010ustar 00000000000000use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _}; // eventually it would be nice to make this a const function so that we could // just get the version from a variable directly, but this is fine for now pub fn version() -> u32 { let major = env!("CARGO_PKG_VERSION_MAJOR"); let minor = env!("CARGO_PKG_VERSION_MINOR"); let patch = env!("CARGO_PKG_VERSION_PATCH"); major.parse::().unwrap() * 1_000_000 + minor.parse::().unwrap() * 1_000 + patch.parse::().unwrap() } #[derive(serde::Serialize, serde::Deserialize, Debug)] pub struct Request { tty: Option, environment: Option, pub action: Action, } impl Request { pub fn new(environment: Environment, action: Action) -> Self { Self { tty: None, environment: Some(environment), action, } } pub fn environment(self) -> Environment { self.environment.unwrap_or_else(|| Environment { tty: self.tty.map(|tty| SerializableOsString(tty.into())), env_vars: vec![], }) } } // Taken from https://github.com/gpg/gnupg/blob/36dbca3e6944d13e75e96eace634e58a7d7e201d/common/session-env.c#L62-L91 pub const ENVIRONMENT_VARIABLES: &[&str] = &[ // Used to set ttytype "TERM", // The X display "DISPLAY", // Xlib Authentication "XAUTHORITY", // Used by Xlib to select X input modules (e.g. "@im=SCIM") "XMODIFIERS", // For the Wayland display engine. "WAYLAND_DISPLAY", // Used by Qt and other non-GTK toolkits to check for X11 or Wayland "XDG_SESSION_TYPE", // Used by Qt to explicitly request X11 or Wayland; in particular, needed to // make Qt use Wayland on GNOME "QT_QPA_PLATFORM", // Used by GTK to select GTK input modules (e.g. "scim-bridge") "GTK_IM_MODULE", // Used by GNOME 3 to talk to gcr over dbus "DBUS_SESSION_BUS_ADDRESS", // Used by Qt to select Qt input modules (e.g. "xim") "QT_IM_MODULE", // Used for communication with non-standard Pinentries "PINENTRY_USER_DATA", // Used to pass window information "PINENTRY_GEOM_HINT", ]; pub static ENVIRONMENT_VARIABLES_OS: std::sync::LazyLock< Vec, > = std::sync::LazyLock::new(|| { ENVIRONMENT_VARIABLES .iter() .map(std::ffi::OsString::from) .collect() }); #[derive(Hash, PartialEq, Eq, Debug)] struct SerializableOsString(std::ffi::OsString); impl serde::Serialize for SerializableOsString { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_bytes(self.0.as_bytes()) } } impl<'de> serde::Deserialize<'de> for SerializableOsString { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { type Value = SerializableOsString; fn expecting( &self, formatter: &mut std::fmt::Formatter, ) -> std::fmt::Result { formatter.write_str("os string") } fn visit_seq( self, mut access: S, ) -> Result where S: serde::de::SeqAccess<'de>, { let mut bytes = Vec::with_capacity(access.size_hint().unwrap_or(0)); while let Some(b) = access.next_element()? { bytes.push(b); } Ok(SerializableOsString(std::ffi::OsString::from_vec(bytes))) } } deserializer.deserialize_bytes(Visitor) } } #[derive(serde::Serialize, serde::Deserialize, Debug)] pub struct Environment { tty: Option, env_vars: Vec<(SerializableOsString, SerializableOsString)>, } impl Environment { pub fn new( tty: Option, env_vars: Vec<(std::ffi::OsString, std::ffi::OsString)>, ) -> Self { Self { tty: tty.map(SerializableOsString), env_vars: env_vars .into_iter() .map(|(k, v)| { (SerializableOsString(k), SerializableOsString(v)) }) .collect(), } } pub fn tty(&self) -> Option<&std::ffi::OsStr> { self.tty.as_ref().map(|tty| tty.0.as_os_str()) } pub fn env_vars( &self, ) -> std::collections::HashMap { self.env_vars .iter() .map(|(var, val)| (var.0.clone(), val.0.clone())) .filter(|(var, _)| (*ENVIRONMENT_VARIABLES_OS).contains(var)) .collect() } } #[derive(serde::Serialize, serde::Deserialize, Debug)] #[serde(tag = "type")] pub enum Action { Login, Register, Unlock, CheckLock, Lock, Sync, Decrypt { cipherstring: String, entry_key: Option, org_id: Option, }, Encrypt { plaintext: String, org_id: Option, }, ClipboardStore { text: String, }, Quit, Version, } #[derive(serde::Serialize, serde::Deserialize, Debug)] #[serde(tag = "type")] pub enum Response { Ack, Error { error: String }, Decrypt { plaintext: String }, Encrypt { cipherstring: String }, Version { version: u32 }, } rbw-1.13.2/src/pwgen.rs000064400000000000000000000054461046102023000127700ustar 00000000000000use rand::seq::SliceRandom as _; const SYMBOLS: &[u8] = b"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; const NUMBERS: &[u8] = b"0123456789"; const LETTERS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const NONCONFUSABLES: &[u8] = b"34678abcdefhjkmnpqrtuwxy"; #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum Type { AllChars, NoSymbols, Numbers, NonConfusables, Diceware, } pub fn pwgen(ty: Type, len: usize) -> String { let mut rng = rand::thread_rng(); let alphabet = match ty { Type::AllChars => { let mut v = vec![]; v.extend(SYMBOLS.iter().copied()); v.extend(NUMBERS.iter().copied()); v.extend(LETTERS.iter().copied()); v } Type::NoSymbols => { let mut v = vec![]; v.extend(NUMBERS.iter().copied()); v.extend(LETTERS.iter().copied()); v } Type::Numbers => { let mut v = vec![]; v.extend(NUMBERS.iter().copied()); v } Type::NonConfusables => { let mut v = vec![]; v.extend(NONCONFUSABLES.iter().copied()); v } Type::Diceware => { return diceware(&mut rng, len); } }; let mut pass = vec![]; pass.extend( std::iter::repeat_with(|| alphabet.choose(&mut rng).unwrap()) .take(len), ); // unwrap is safe because the method of generating passwords guarantees // valid utf8 String::from_utf8(pass).unwrap() } fn diceware(rng: &mut impl rand::RngCore, len: usize) -> String { let mut words = vec![]; for _ in 0..len { // unwrap is safe because choose only returns None for an empty slice words.push(*crate::wordlist::EFF_LONG.choose(rng).unwrap()); } words.join(" ") } #[cfg(test)] mod test { use super::*; #[test] fn test_pwgen() { let pw = pwgen(Type::AllChars, 50); assert_eq!(pw.len(), 50); // technically this could fail, but the chances are incredibly low // (around 0.000009%) assert_duplicates(&pw); let pw = pwgen(Type::AllChars, 100); assert_eq!(pw.len(), 100); assert_duplicates(&pw); let pw = pwgen(Type::NoSymbols, 100); assert_eq!(pw.len(), 100); assert_duplicates(&pw); let pw = pwgen(Type::Numbers, 100); assert_eq!(pw.len(), 100); assert_duplicates(&pw); let pw = pwgen(Type::NonConfusables, 100); assert_eq!(pw.len(), 100); assert_duplicates(&pw); } #[track_caller] fn assert_duplicates(s: &str) { let mut set = std::collections::HashSet::new(); for c in s.chars() { set.insert(c); } assert!(set.len() < s.len()); } } rbw-1.13.2/src/wordlist.rs000064400000000000000000003436041046102023000135200ustar 00000000000000pub const EFF_LONG: &[&str] = &[ "abacus", "abdomen", "abdominal", "abide", "abiding", "ability", "ablaze", "able", "abnormal", "abrasion", "abrasive", "abreast", "abridge", "abroad", "abruptly", "absence", "absentee", "absently", "absinthe", "absolute", "absolve", "abstain", "abstract", "absurd", "accent", "acclaim", "acclimate", "accompany", "account", "accuracy", "accurate", "accustom", "acetone", "achiness", "aching", "acid", "acorn", "acquaint", "acquire", "acre", "acrobat", "acronym", "acting", "action", "activate", "activator", "active", "activism", "activist", "activity", "actress", "acts", "acutely", "acuteness", "aeration", "aerobics", "aerosol", "aerospace", "afar", "affair", "affected", "affecting", "affection", "affidavit", "affiliate", "affirm", "affix", "afflicted", "affluent", "afford", "affront", "aflame", "afloat", "aflutter", "afoot", "afraid", "afterglow", "afterlife", "aftermath", "aftermost", "afternoon", "aged", "ageless", "agency", "agenda", "agent", "aggregate", "aghast", "agile", "agility", "aging", "agnostic", "agonize", "agonizing", "agony", "agreeable", "agreeably", "agreed", "agreeing", "agreement", "aground", "ahead", "ahoy", "aide", "aids", "aim", "ajar", "alabaster", "alarm", "albatross", "album", "alfalfa", "algebra", "algorithm", "alias", "alibi", "alienable", "alienate", "aliens", "alike", "alive", "alkaline", "alkalize", "almanac", "almighty", "almost", "aloe", "aloft", "aloha", "alone", "alongside", "aloof", "alphabet", "alright", "although", "altitude", "alto", "aluminum", "alumni", "always", "amaretto", "amaze", "amazingly", "amber", "ambiance", "ambiguity", "ambiguous", "ambition", "ambitious", "ambulance", "ambush", "amendable", "amendment", "amends", "amenity", "amiable", "amicably", "amid", "amigo", "amino", "amiss", "ammonia", "ammonium", "amnesty", "amniotic", "among", "amount", "amperage", "ample", "amplifier", "amplify", "amply", "amuck", "amulet", "amusable", "amused", "amusement", "amuser", "amusing", "anaconda", "anaerobic", "anagram", "anatomist", "anatomy", "anchor", "anchovy", "ancient", "android", "anemia", "anemic", "aneurism", "anew", "angelfish", "angelic", "anger", "angled", "angler", "angles", "angling", "angrily", "angriness", "anguished", "angular", "animal", "animate", "animating", "animation", "animator", "anime", "animosity", "ankle", "annex", "annotate", "announcer", "annoying", "annually", "annuity", "anointer", "another", "answering", "antacid", "antarctic", "anteater", "antelope", "antennae", "anthem", "anthill", "anthology", "antibody", "antics", "antidote", "antihero", "antiquely", "antiques", "antiquity", "antirust", "antitoxic", "antitrust", "antiviral", "antivirus", "antler", "antonym", "antsy", "anvil", "anybody", "anyhow", "anymore", "anyone", "anyplace", "anything", "anytime", "anyway", "anywhere", "aorta", "apache", "apostle", "appealing", "appear", "appease", "appeasing", "appendage", "appendix", "appetite", "appetizer", "applaud", "applause", "apple", "appliance", "applicant", "applied", "apply", "appointee", "appraisal", "appraiser", "apprehend", "approach", "approval", "approve", "apricot", "april", "apron", "aptitude", "aptly", "aqua", "aqueduct", "arbitrary", "arbitrate", "ardently", "area", "arena", "arguable", "arguably", "argue", "arise", "armadillo", "armband", "armchair", "armed", "armful", "armhole", "arming", "armless", "armoire", "armored", "armory", "armrest", "army", "aroma", "arose", "around", "arousal", "arrange", "array", "arrest", "arrival", "arrive", "arrogance", "arrogant", "arson", "art", "ascend", "ascension", "ascent", "ascertain", "ashamed", "ashen", "ashes", "ashy", "aside", "askew", "asleep", "asparagus", "aspect", "aspirate", "aspire", "aspirin", "astonish", "astound", "astride", "astrology", "astronaut", "astronomy", "astute", "atlantic", "atlas", "atom", "atonable", "atop", "atrium", "atrocious", "atrophy", "attach", "attain", "attempt", "attendant", "attendee", "attention", "attentive", "attest", "attic", "attire", "attitude", "attractor", "attribute", "atypical", "auction", "audacious", "audacity", "audible", "audibly", "audience", "audio", "audition", "augmented", "august", "authentic", "author", "autism", "autistic", "autograph", "automaker", "automated", "automatic", "autopilot", "available", "avalanche", "avatar", "avenge", "avenging", "avenue", "average", "aversion", "avert", "aviation", "aviator", "avid", "avoid", "await", "awaken", "award", "aware", "awhile", "awkward", "awning", "awoke", "awry", "axis", "babble", "babbling", "babied", "baboon", "backache", "backboard", "backboned", "backdrop", "backed", "backer", "backfield", "backfire", "backhand", "backing", "backlands", "backlash", "backless", "backlight", "backlit", "backlog", "backpack", "backpedal", "backrest", "backroom", "backshift", "backside", "backslid", "backspace", "backspin", "backstab", "backstage", "backtalk", "backtrack", "backup", "backward", "backwash", "backwater", "backyard", "bacon", "bacteria", "bacterium", "badass", "badge", "badland", "badly", "badness", "baffle", "baffling", "bagel", "bagful", "baggage", "bagged", "baggie", "bagginess", "bagging", "baggy", "bagpipe", "baguette", "baked", "bakery", "bakeshop", "baking", "balance", "balancing", "balcony", "balmy", "balsamic", "bamboo", "banana", "banish", "banister", "banjo", "bankable", "bankbook", "banked", "banker", "banking", "banknote", "bankroll", "banner", "bannister", "banshee", "banter", "barbecue", "barbed", "barbell", "barber", "barcode", "barge", "bargraph", "barista", "baritone", "barley", "barmaid", "barman", "barn", "barometer", "barrack", "barracuda", "barrel", "barrette", "barricade", "barrier", "barstool", "bartender", "barterer", "bash", "basically", "basics", "basil", "basin", "basis", "basket", "batboy", "batch", "bath", "baton", "bats", "battalion", "battered", "battering", "battery", "batting", "battle", "bauble", "bazooka", "blabber", "bladder", "blade", "blah", "blame", "blaming", "blanching", "blandness", "blank", "blaspheme", "blasphemy", "blast", "blatancy", "blatantly", "blazer", "blazing", "bleach", "bleak", "bleep", "blemish", "blend", "bless", "blighted", "blimp", "bling", "blinked", "blinker", "blinking", "blinks", "blip", "blissful", "blitz", "blizzard", "bloated", "bloating", "blob", "blog", "bloomers", "blooming", "blooper", "blot", "blouse", "blubber", "bluff", "bluish", "blunderer", "blunt", "blurb", "blurred", "blurry", "blurt", "blush", "blustery", "boaster", "boastful", "boasting", "boat", "bobbed", "bobbing", "bobble", "bobcat", "bobsled", "bobtail", "bodacious", "body", "bogged", "boggle", "bogus", "boil", "bok", "bolster", "bolt", "bonanza", "bonded", "bonding", "bondless", "boned", "bonehead", "boneless", "bonelike", "boney", "bonfire", "bonnet", "bonsai", "bonus", "bony", "boogeyman", "boogieman", "book", "boondocks", "booted", "booth", "bootie", "booting", "bootlace", "bootleg", "boots", "boozy", "borax", "boring", "borough", "borrower", "borrowing", "boss", "botanical", "botanist", "botany", "botch", "both", "bottle", "bottling", "bottom", "bounce", "bouncing", "bouncy", "bounding", "boundless", "bountiful", "bovine", "boxcar", "boxer", "boxing", "boxlike", "boxy", "breach", "breath", "breeches", "breeching", "breeder", "breeding", "breeze", "breezy", "brethren", "brewery", "brewing", "briar", "bribe", "brick", "bride", "bridged", "brigade", "bright", "brilliant", "brim", "bring", "brink", "brisket", "briskly", "briskness", "bristle", "brittle", "broadband", "broadcast", "broaden", "broadly", "broadness", "broadside", "broadways", "broiler", "broiling", "broken", "broker", "bronchial", "bronco", "bronze", "bronzing", "brook", "broom", "brought", "browbeat", "brownnose", "browse", "browsing", "bruising", "brunch", "brunette", "brunt", "brush", "brussels", "brute", "brutishly", "bubble", "bubbling", "bubbly", "buccaneer", "bucked", "bucket", "buckle", "buckshot", "buckskin", "bucktooth", "buckwheat", "buddhism", "buddhist", "budding", "buddy", "budget", "buffalo", "buffed", "buffer", "buffing", "buffoon", "buggy", "bulb", "bulge", "bulginess", "bulgur", "bulk", "bulldog", "bulldozer", "bullfight", "bullfrog", "bullhorn", "bullion", "bullish", "bullpen", "bullring", "bullseye", "bullwhip", "bully", "bunch", "bundle", "bungee", "bunion", "bunkbed", "bunkhouse", "bunkmate", "bunny", "bunt", "busboy", "bush", "busily", "busload", "bust", "busybody", "buzz", "cabana", "cabbage", "cabbie", "cabdriver", "cable", "caboose", "cache", "cackle", "cacti", "cactus", "caddie", "caddy", "cadet", "cadillac", "cadmium", "cage", "cahoots", "cake", "calamari", "calamity", "calcium", "calculate", "calculus", "caliber", "calibrate", "calm", "caloric", "calorie", "calzone", "camcorder", "cameo", "camera", "camisole", "camper", "campfire", "camping", "campsite", "campus", "canal", "canary", "cancel", "candied", "candle", "candy", "cane", "canine", "canister", "cannabis", "canned", "canning", "cannon", "cannot", "canola", "canon", "canopener", "canopy", "canteen", "canyon", "capable", "capably", "capacity", "cape", "capillary", "capital", "capitol", "capped", "capricorn", "capsize", "capsule", "caption", "captivate", "captive", "captivity", "capture", "caramel", "carat", "caravan", "carbon", "cardboard", "carded", "cardiac", "cardigan", "cardinal", "cardstock", "carefully", "caregiver", "careless", "caress", "caretaker", "cargo", "caring", "carless", "carload", "carmaker", "carnage", "carnation", "carnival", "carnivore", "carol", "carpenter", "carpentry", "carpool", "carport", "carried", "carrot", "carrousel", "carry", "cartel", "cartload", "carton", "cartoon", "cartridge", "cartwheel", "carve", "carving", "carwash", "cascade", "case", "cash", "casing", "casino", "casket", "cassette", "casually", "casualty", "catacomb", "catalog", "catalyst", "catalyze", "catapult", "cataract", "catatonic", "catcall", "catchable", "catcher", "catching", "catchy", "caterer", "catering", "catfight", "catfish", "cathedral", "cathouse", "catlike", "catnap", "catnip", "catsup", "cattail", "cattishly", "cattle", "catty", "catwalk", "caucasian", "caucus", "causal", "causation", "cause", "causing", "cauterize", "caution", "cautious", "cavalier", "cavalry", "caviar", "cavity", "cedar", "celery", "celestial", "celibacy", "celibate", "celtic", "cement", "census", "ceramics", "ceremony", "certainly", "certainty", "certified", "certify", "cesarean", "cesspool", "chafe", "chaffing", "chain", "chair", "chalice", "challenge", "chamber", "chamomile", "champion", "chance", "change", "channel", "chant", "chaos", "chaperone", "chaplain", "chapped", "chaps", "chapter", "character", "charbroil", "charcoal", "charger", "charging", "chariot", "charity", "charm", "charred", "charter", "charting", "chase", "chasing", "chaste", "chastise", "chastity", "chatroom", "chatter", "chatting", "chatty", "cheating", "cheddar", "cheek", "cheer", "cheese", "cheesy", "chef", "chemicals", "chemist", "chemo", "cherisher", "cherub", "chess", "chest", "chevron", "chevy", "chewable", "chewer", "chewing", "chewy", "chief", "chihuahua", "childcare", "childhood", "childish", "childless", "childlike", "chili", "chill", "chimp", "chip", "chirping", "chirpy", "chitchat", "chivalry", "chive", "chloride", "chlorine", "choice", "chokehold", "choking", "chomp", "chooser", "choosing", "choosy", "chop", "chosen", "chowder", "chowtime", "chrome", "chubby", "chuck", "chug", "chummy", "chump", "chunk", "churn", "chute", "cider", "cilantro", "cinch", "cinema", "cinnamon", "circle", "circling", "circular", "circulate", "circus", "citable", "citadel", "citation", "citizen", "citric", "citrus", "city", "civic", "civil", "clad", "claim", "clambake", "clammy", "clamor", "clamp", "clamshell", "clang", "clanking", "clapped", "clapper", "clapping", "clarify", "clarinet", "clarity", "clash", "clasp", "class", "clatter", "clause", "clavicle", "claw", "clay", "clean", "clear", "cleat", "cleaver", "cleft", "clench", "clergyman", "clerical", "clerk", "clever", "clicker", "client", "climate", "climatic", "cling", "clinic", "clinking", "clip", "clique", "cloak", "clobber", "clock", "clone", "cloning", "closable", "closure", "clothes", "clothing", "cloud", "clover", "clubbed", "clubbing", "clubhouse", "clump", "clumsily", "clumsy", "clunky", "clustered", "clutch", "clutter", "coach", "coagulant", "coastal", "coaster", "coasting", "coastland", "coastline", "coat", "coauthor", "cobalt", "cobbler", "cobweb", "cocoa", "coconut", "cod", "coeditor", "coerce", "coexist", "coffee", "cofounder", "cognition", "cognitive", "cogwheel", "coherence", "coherent", "cohesive", "coil", "coke", "cola", "cold", "coleslaw", "coliseum", "collage", "collapse", "collar", "collected", "collector", "collide", "collie", "collision", "colonial", "colonist", "colonize", "colony", "colossal", "colt", "coma", "come", "comfort", "comfy", "comic", "coming", "comma", "commence", "commend", "comment", "commerce", "commode", "commodity", "commodore", "common", "commotion", "commute", "commuting", "compacted", "compacter", "compactly", "compactor", "companion", "company", "compare", "compel", "compile", "comply", "component", "composed", "composer", "composite", "compost", "composure", "compound", "compress", "comprised", "computer", "computing", "comrade", "concave", "conceal", "conceded", "concept", "concerned", "concert", "conch", "concierge", "concise", "conclude", "concrete", "concur", "condense", "condiment", "condition", "condone", "conducive", "conductor", "conduit", "cone", "confess", "confetti", "confidant", "confident", "confider", "confiding", "configure", "confined", "confining", "confirm", "conflict", "conform", "confound", "confront", "confused", "confusing", "confusion", "congenial", "congested", "congrats", "congress", "conical", "conjoined", "conjure", "conjuror", "connected", "connector", "consensus", "consent", "console", "consoling", "consonant", "constable", "constant", "constrain", "constrict", "construct", "consult", "consumer", "consuming", "contact", "container", "contempt", "contend", "contented", "contently", "contents", "contest", "context", "contort", "contour", "contrite", "control", "contusion", "convene", "convent", "copartner", "cope", "copied", "copier", "copilot", "coping", "copious", "copper", "copy", "coral", "cork", "cornball", "cornbread", "corncob", "cornea", "corned", "corner", "cornfield", "cornflake", "cornhusk", "cornmeal", "cornstalk", "corny", "coronary", "coroner", "corporal", "corporate", "corral", "correct", "corridor", "corrode", "corroding", "corrosive", "corsage", "corset", "cortex", "cosigner", "cosmetics", "cosmic", "cosmos", "cosponsor", "cost", "cottage", "cotton", "couch", "cough", "could", "countable", "countdown", "counting", "countless", "country", "county", "courier", "covenant", "cover", "coveted", "coveting", "coyness", "cozily", "coziness", "cozy", "crabbing", "crabgrass", "crablike", "crabmeat", "cradle", "cradling", "crafter", "craftily", "craftsman", "craftwork", "crafty", "cramp", "cranberry", "crane", "cranial", "cranium", "crank", "crate", "crave", "craving", "crawfish", "crawlers", "crawling", "crayfish", "crayon", "crazed", "crazily", "craziness", "crazy", "creamed", "creamer", "creamlike", "crease", "creasing", "creatable", "create", "creation", "creative", "creature", "credible", "credibly", "credit", "creed", "creme", "creole", "crepe", "crept", "crescent", "crested", "cresting", "crestless", "crevice", "crewless", "crewman", "crewmate", "crib", "cricket", "cried", "crier", "crimp", "crimson", "cringe", "cringing", "crinkle", "crinkly", "crisped", "crisping", "crisply", "crispness", "crispy", "criteria", "critter", "croak", "crock", "crook", "croon", "crop", "cross", "crouch", "crouton", "crowbar", "crowd", "crown", "crucial", "crudely", "crudeness", "cruelly", "cruelness", "cruelty", "crumb", "crummiest", "crummy", "crumpet", "crumpled", "cruncher", "crunching", "crunchy", "crusader", "crushable", "crushed", "crusher", "crushing", "crust", "crux", "crying", "cryptic", "crystal", "cubbyhole", "cube", "cubical", "cubicle", "cucumber", "cuddle", "cuddly", "cufflink", "culinary", "culminate", "culpable", "culprit", "cultivate", "cultural", "culture", "cupbearer", "cupcake", "cupid", "cupped", "cupping", "curable", "curator", "curdle", "cure", "curfew", "curing", "curled", "curler", "curliness", "curling", "curly", "curry", "curse", "cursive", "cursor", "curtain", "curtly", "curtsy", "curvature", "curve", "curvy", "cushy", "cusp", "cussed", "custard", "custodian", "custody", "customary", "customer", "customize", "customs", "cut", "cycle", "cyclic", "cycling", "cyclist", "cylinder", "cymbal", "cytoplasm", "cytoplast", "dab", "dad", "daffodil", "dagger", "daily", "daintily", "dainty", "dairy", "daisy", "dallying", "dance", "dancing", "dandelion", "dander", "dandruff", "dandy", "danger", "dangle", "dangling", "daredevil", "dares", "daringly", "darkened", "darkening", "darkish", "darkness", "darkroom", "darling", "darn", "dart", "darwinism", "dash", "dastardly", "data", "datebook", "dating", "daughter", "daunting", "dawdler", "dawn", "daybed", "daybreak", "daycare", "daydream", "daylight", "daylong", "dayroom", "daytime", "dazzler", "dazzling", "deacon", "deafening", "deafness", "dealer", "dealing", "dealmaker", "dealt", "dean", "debatable", "debate", "debating", "debit", "debrief", "debtless", "debtor", "debug", "debunk", "decade", "decaf", "decal", "decathlon", "decay", "deceased", "deceit", "deceiver", "deceiving", "december", "decency", "decent", "deception", "deceptive", "decibel", "decidable", "decimal", "decimeter", "decipher", "deck", "declared", "decline", "decode", "decompose", "decorated", "decorator", "decoy", "decrease", "decree", "dedicate", "dedicator", "deduce", "deduct", "deed", "deem", "deepen", "deeply", "deepness", "deface", "defacing", "defame", "default", "defeat", "defection", "defective", "defendant", "defender", "defense", "defensive", "deferral", "deferred", "defiance", "defiant", "defile", "defiling", "define", "definite", "deflate", "deflation", "deflator", "deflected", "deflector", "defog", "deforest", "defraud", "defrost", "deftly", "defuse", "defy", "degraded", "degrading", "degrease", "degree", "dehydrate", "deity", "dejected", "delay", "delegate", "delegator", "delete", "deletion", "delicacy", "delicate", "delicious", "delighted", "delirious", "delirium", "deliverer", "delivery", "delouse", "delta", "deluge", "delusion", "deluxe", "demanding", "demeaning", "demeanor", "demise", "democracy", "democrat", "demote", "demotion", "demystify", "denatured", "deniable", "denial", "denim", "denote", "dense", "density", "dental", "dentist", "denture", "deny", "deodorant", "deodorize", "departed", "departure", "depict", "deplete", "depletion", "deplored", "deploy", "deport", "depose", "depraved", "depravity", "deprecate", "depress", "deprive", "depth", "deputize", "deputy", "derail", "deranged", "derby", "derived", "desecrate", "deserve", "deserving", "designate", "designed", "designer", "designing", "deskbound", "desktop", "deskwork", "desolate", "despair", "despise", "despite", "destiny", "destitute", "destruct", "detached", "detail", "detection", "detective", "detector", "detention", "detergent", "detest", "detonate", "detonator", "detoxify", "detract", "deuce", "devalue", "deviancy", "deviant", "deviate", "deviation", "deviator", "device", "devious", "devotedly", "devotee", "devotion", "devourer", "devouring", "devoutly", "dexterity", "dexterous", "diabetes", "diabetic", "diabolic", "diagnoses", "diagnosis", "diagram", "dial", "diameter", "diaper", "diaphragm", "diary", "dice", "dicing", "dictate", "dictation", "dictator", "difficult", "diffused", "diffuser", "diffusion", "diffusive", "dig", "dilation", "diligence", "diligent", "dill", "dilute", "dime", "diminish", "dimly", "dimmed", "dimmer", "dimness", "dimple", "diner", "dingbat", "dinghy", "dinginess", "dingo", "dingy", "dining", "dinner", "diocese", "dioxide", "diploma", "dipped", "dipper", "dipping", "directed", "direction", "directive", "directly", "directory", "direness", "dirtiness", "disabled", "disagree", "disallow", "disarm", "disarray", "disaster", "disband", "disbelief", "disburse", "discard", "discern", "discharge", "disclose", "discolor", "discount", "discourse", "discover", "discuss", "disdain", "disengage", "disfigure", "disgrace", "dish", "disinfect", "disjoin", "disk", "dislike", "disliking", "dislocate", "dislodge", "disloyal", "dismantle", "dismay", "dismiss", "dismount", "disobey", "disorder", "disown", "disparate", "disparity", "dispatch", "dispense", "dispersal", "dispersed", "disperser", "displace", "display", "displease", "disposal", "dispose", "disprove", "dispute", "disregard", "disrupt", "dissuade", "distance", "distant", "distaste", "distill", "distinct", "distort", "distract", "distress", "district", "distrust", "ditch", "ditto", "ditzy", "dividable", "divided", "dividend", "dividers", "dividing", "divinely", "diving", "divinity", "divisible", "divisibly", "division", "divisive", "divorcee", "dizziness", "dizzy", "doable", "docile", "dock", "doctrine", "document", "dodge", "dodgy", "doily", "doing", "dole", "dollar", "dollhouse", "dollop", "dolly", "dolphin", "domain", "domelike", "domestic", "dominion", "dominoes", "donated", "donation", "donator", "donor", "donut", "doodle", "doorbell", "doorframe", "doorknob", "doorman", "doormat", "doornail", "doorpost", "doorstep", "doorstop", "doorway", "doozy", "dork", "dormitory", "dorsal", "dosage", "dose", "dotted", "doubling", "douche", "dove", "down", "dowry", "doze", "drab", "dragging", "dragonfly", "dragonish", "dragster", "drainable", "drainage", "drained", "drainer", "drainpipe", "dramatic", "dramatize", "drank", "drapery", "drastic", "draw", "dreaded", "dreadful", "dreadlock", "dreamboat", "dreamily", "dreamland", "dreamless", "dreamlike", "dreamt", "dreamy", "drearily", "dreary", "drench", "dress", "drew", "dribble", "dried", "drier", "drift", "driller", "drilling", "drinkable", "drinking", "dripping", "drippy", "drivable", "driven", "driver", "driveway", "driving", "drizzle", "drizzly", "drone", "drool", "droop", "drop-down", "dropbox", "dropkick", "droplet", "dropout", "dropper", "drove", "drown", "drowsily", "drudge", "drum", "dry", "dubbed", "dubiously", "duchess", "duckbill", "ducking", "duckling", "ducktail", "ducky", "duct", "dude", "duffel", "dugout", "duh", "duke", "duller", "dullness", "duly", "dumping", "dumpling", "dumpster", "duo", "dupe", "duplex", "duplicate", "duplicity", "durable", "durably", "duration", "duress", "during", "dusk", "dust", "dutiful", "duty", "duvet", "dwarf", "dweeb", "dwelled", "dweller", "dwelling", "dwindle", "dwindling", "dynamic", "dynamite", "dynasty", "dyslexia", "dyslexic", "each", "eagle", "earache", "eardrum", "earflap", "earful", "earlobe", "early", "earmark", "earmuff", "earphone", "earpiece", "earplugs", "earring", "earshot", "earthen", "earthlike", "earthling", "earthly", "earthworm", "earthy", "earwig", "easeful", "easel", "easiest", "easily", "easiness", "easing", "eastbound", "eastcoast", "easter", "eastward", "eatable", "eaten", "eatery", "eating", "eats", "ebay", "ebony", "ebook", "ecard", "eccentric", "echo", "eclair", "eclipse", "ecologist", "ecology", "economic", "economist", "economy", "ecosphere", "ecosystem", "edge", "edginess", "edging", "edgy", "edition", "editor", "educated", "education", "educator", "eel", "effective", "effects", "efficient", "effort", "eggbeater", "egging", "eggnog", "eggplant", "eggshell", "egomaniac", "egotism", "egotistic", "either", "eject", "elaborate", "elastic", "elated", "elbow", "eldercare", "elderly", "eldest", "electable", "election", "elective", "elephant", "elevate", "elevating", "elevation", "elevator", "eleven", "elf", "eligible", "eligibly", "eliminate", "elite", "elitism", "elixir", "elk", "ellipse", "elliptic", "elm", "elongated", "elope", "eloquence", "eloquent", "elsewhere", "elude", "elusive", "elves", "email", "embargo", "embark", "embassy", "embattled", "embellish", "ember", "embezzle", "emblaze", "emblem", "embody", "embolism", "emboss", "embroider", "emcee", "emerald", "emergency", "emission", "emit", "emote", "emoticon", "emotion", "empathic", "empathy", "emperor", "emphases", "emphasis", "emphasize", "emphatic", "empirical", "employed", "employee", "employer", "emporium", "empower", "emptier", "emptiness", "empty", "emu", "enable", "enactment", "enamel", "enchanted", "enchilada", "encircle", "enclose", "enclosure", "encode", "encore", "encounter", "encourage", "encroach", "encrust", "encrypt", "endanger", "endeared", "endearing", "ended", "ending", "endless", "endnote", "endocrine", "endorphin", "endorse", "endowment", "endpoint", "endurable", "endurance", "enduring", "energetic", "energize", "energy", "enforced", "enforcer", "engaged", "engaging", "engine", "engorge", "engraved", "engraver", "engraving", "engross", "engulf", "enhance", "enigmatic", "enjoyable", "enjoyably", "enjoyer", "enjoying", "enjoyment", "enlarged", "enlarging", "enlighten", "enlisted", "enquirer", "enrage", "enrich", "enroll", "enslave", "ensnare", "ensure", "entail", "entangled", "entering", "entertain", "enticing", "entire", "entitle", "entity", "entomb", "entourage", "entrap", "entree", "entrench", "entrust", "entryway", "entwine", "enunciate", "envelope", "enviable", "enviably", "envious", "envision", "envoy", "envy", "enzyme", "epic", "epidemic", "epidermal", "epidermis", "epidural", "epilepsy", "epileptic", "epilogue", "epiphany", "episode", "equal", "equate", "equation", "equator", "equinox", "equipment", "equity", "equivocal", "eradicate", "erasable", "erased", "eraser", "erasure", "ergonomic", "errand", "errant", "erratic", "error", "erupt", "escalate", "escalator", "escapable", "escapade", "escapist", "escargot", "eskimo", "esophagus", "espionage", "espresso", "esquire", "essay", "essence", "essential", "establish", "estate", "esteemed", "estimate", "estimator", "estranged", "estrogen", "etching", "eternal", "eternity", "ethanol", "ether", "ethically", "ethics", "euphemism", "evacuate", "evacuee", "evade", "evaluate", "evaluator", "evaporate", "evasion", "evasive", "even", "everglade", "evergreen", "everybody", "everyday", "everyone", "evict", "evidence", "evident", "evil", "evoke", "evolution", "evolve", "exact", "exalted", "example", "excavate", "excavator", "exceeding", "exception", "excess", "exchange", "excitable", "exciting", "exclaim", "exclude", "excluding", "exclusion", "exclusive", "excretion", "excretory", "excursion", "excusable", "excusably", "excuse", "exemplary", "exemplify", "exemption", "exerciser", "exert", "exes", "exfoliate", "exhale", "exhaust", "exhume", "exile", "existing", "exit", "exodus", "exonerate", "exorcism", "exorcist", "expand", "expanse", "expansion", "expansive", "expectant", "expedited", "expediter", "expel", "expend", "expenses", "expensive", "expert", "expire", "expiring", "explain", "expletive", "explicit", "explode", "exploit", "explore", "exploring", "exponent", "exporter", "exposable", "expose", "exposure", "express", "expulsion", "exquisite", "extended", "extending", "extent", "extenuate", "exterior", "external", "extinct", "extortion", "extradite", "extras", "extrovert", "extrude", "extruding", "exuberant", "fable", "fabric", "fabulous", "facebook", "facecloth", "facedown", "faceless", "facelift", "faceplate", "faceted", "facial", "facility", "facing", "facsimile", "faction", "factoid", "factor", "factsheet", "factual", "faculty", "fade", "fading", "failing", "falcon", "fall", "false", "falsify", "fame", "familiar", "family", "famine", "famished", "fanatic", "fancied", "fanciness", "fancy", "fanfare", "fang", "fanning", "fantasize", "fantastic", "fantasy", "fascism", "fastball", "faster", "fasting", "fastness", "faucet", "favorable", "favorably", "favored", "favoring", "favorite", "fax", "feast", "federal", "fedora", "feeble", "feed", "feel", "feisty", "feline", "felt-tip", "feminine", "feminism", "feminist", "feminize", "femur", "fence", "fencing", "fender", "ferment", "fernlike", "ferocious", "ferocity", "ferret", "ferris", "ferry", "fervor", "fester", "festival", "festive", "festivity", "fetal", "fetch", "fever", "fiber", "fiction", "fiddle", "fiddling", "fidelity", "fidgeting", "fidgety", "fifteen", "fifth", "fiftieth", "fifty", "figment", "figure", "figurine", "filing", "filled", "filler", "filling", "film", "filter", "filth", "filtrate", "finale", "finalist", "finalize", "finally", "finance", "financial", "finch", "fineness", "finer", "finicky", "finished", "finisher", "finishing", "finite", "finless", "finlike", "fiscally", "fit", "five", "flaccid", "flagman", "flagpole", "flagship", "flagstick", "flagstone", "flail", "flakily", "flaky", "flame", "flammable", "flanked", "flanking", "flannels", "flap", "flaring", "flashback", "flashbulb", "flashcard", "flashily", "flashing", "flashy", "flask", "flatbed", "flatfoot", "flatly", "flatness", "flatten", "flattered", "flatterer", "flattery", "flattop", "flatware", "flatworm", "flavored", "flavorful", "flavoring", "flaxseed", "fled", "fleshed", "fleshy", "flick", "flier", "flight", "flinch", "fling", "flint", "flip", "flirt", "float", "flock", "flogging", "flop", "floral", "florist", "floss", "flounder", "flyable", "flyaway", "flyer", "flying", "flyover", "flypaper", "foam", "foe", "fog", "foil", "folic", "folk", "follicle", "follow", "fondling", "fondly", "fondness", "fondue", "font", "food", "fool", "footage", "football", "footbath", "footboard", "footer", "footgear", "foothill", "foothold", "footing", "footless", "footman", "footnote", "footpad", "footpath", "footprint", "footrest", "footsie", "footsore", "footwear", "footwork", "fossil", "foster", "founder", "founding", "fountain", "fox", "foyer", "fraction", "fracture", "fragile", "fragility", "fragment", "fragrance", "fragrant", "frail", "frame", "framing", "frantic", "fraternal", "frayed", "fraying", "frays", "freckled", "freckles", "freebase", "freebee", "freebie", "freedom", "freefall", "freehand", "freeing", "freeload", "freely", "freemason", "freeness", "freestyle", "freeware", "freeway", "freewill", "freezable", "freezing", "freight", "french", "frenzied", "frenzy", "frequency", "frequent", "fresh", "fretful", "fretted", "friction", "friday", "fridge", "fried", "friend", "frighten", "frightful", "frigidity", "frigidly", "frill", "fringe", "frisbee", "frisk", "fritter", "frivolous", "frolic", "from", "front", "frostbite", "frosted", "frostily", "frosting", "frostlike", "frosty", "froth", "frown", "frozen", "fructose", "frugality", "frugally", "fruit", "frustrate", "frying", "gab", "gaffe", "gag", "gainfully", "gaining", "gains", "gala", "gallantly", "galleria", "gallery", "galley", "gallon", "gallows", "gallstone", "galore", "galvanize", "gambling", "game", "gaming", "gamma", "gander", "gangly", "gangrene", "gangway", "gap", "garage", "garbage", "garden", "gargle", "garland", "garlic", "garment", "garnet", "garnish", "garter", "gas", "gatherer", "gathering", "gating", "gauging", "gauntlet", "gauze", "gave", "gawk", "gazing", "gear", "gecko", "geek", "geiger", "gem", "gender", "generic", "generous", "genetics", "genre", "gentile", "gentleman", "gently", "gents", "geography", "geologic", "geologist", "geology", "geometric", "geometry", "geranium", "gerbil", "geriatric", "germicide", "germinate", "germless", "germproof", "gestate", "gestation", "gesture", "getaway", "getting", "getup", "giant", "gibberish", "giblet", "giddily", "giddiness", "giddy", "gift", "gigabyte", "gigahertz", "gigantic", "giggle", "giggling", "giggly", "gigolo", "gilled", "gills", "gimmick", "girdle", "giveaway", "given", "giver", "giving", "gizmo", "gizzard", "glacial", "glacier", "glade", "gladiator", "gladly", "glamorous", "glamour", "glance", "glancing", "glandular", "glare", "glaring", "glass", "glaucoma", "glazing", "gleaming", "gleeful", "glider", "gliding", "glimmer", "glimpse", "glisten", "glitch", "glitter", "glitzy", "gloater", "gloating", "gloomily", "gloomy", "glorified", "glorifier", "glorify", "glorious", "glory", "gloss", "glove", "glowing", "glowworm", "glucose", "glue", "gluten", "glutinous", "glutton", "gnarly", "gnat", "goal", "goatskin", "goes", "goggles", "going", "goldfish", "goldmine", "goldsmith", "golf", "goliath", "gonad", "gondola", "gone", "gong", "good", "gooey", "goofball", "goofiness", "goofy", "google", "goon", "gopher", "gore", "gorged", "gorgeous", "gory", "gosling", "gossip", "gothic", "gotten", "gout", "gown", "grab", "graceful", "graceless", "gracious", "gradation", "graded", "grader", "gradient", "grading", "gradually", "graduate", "graffiti", "grafted", "grafting", "grain", "granddad", "grandkid", "grandly", "grandma", "grandpa", "grandson", "granite", "granny", "granola", "grant", "granular", "grape", "graph", "grapple", "grappling", "grasp", "grass", "gratified", "gratify", "grating", "gratitude", "gratuity", "gravel", "graveness", "graves", "graveyard", "gravitate", "gravity", "gravy", "gray", "grazing", "greasily", "greedily", "greedless", "greedy", "green", "greeter", "greeting", "grew", "greyhound", "grid", "grief", "grievance", "grieving", "grievous", "grill", "grimace", "grimacing", "grime", "griminess", "grimy", "grinch", "grinning", "grip", "gristle", "grit", "groggily", "groggy", "groin", "groom", "groove", "grooving", "groovy", "grope", "ground", "grouped", "grout", "grove", "grower", "growing", "growl", "grub", "grudge", "grudging", "grueling", "gruffly", "grumble", "grumbling", "grumbly", "grumpily", "grunge", "grunt", "guacamole", "guidable", "guidance", "guide", "guiding", "guileless", "guise", "gulf", "gullible", "gully", "gulp", "gumball", "gumdrop", "gumminess", "gumming", "gummy", "gurgle", "gurgling", "guru", "gush", "gusto", "gusty", "gutless", "guts", "gutter", "guy", "guzzler", "gyration", "habitable", "habitant", "habitat", "habitual", "hacked", "hacker", "hacking", "hacksaw", "had", "haggler", "haiku", "half", "halogen", "halt", "halved", "halves", "hamburger", "hamlet", "hammock", "hamper", "hamster", "hamstring", "handbag", "handball", "handbook", "handbrake", "handcart", "handclap", "handclasp", "handcraft", "handcuff", "handed", "handful", "handgrip", "handgun", "handheld", "handiness", "handiwork", "handlebar", "handled", "handler", "handling", "handmade", "handoff", "handpick", "handprint", "handrail", "handsaw", "handset", "handsfree", "handshake", "handstand", "handwash", "handwork", "handwoven", "handwrite", "handyman", "hangnail", "hangout", "hangover", "hangup", "hankering", "hankie", "hanky", "haphazard", "happening", "happier", "happiest", "happily", "happiness", "happy", "harbor", "hardcopy", "hardcore", "hardcover", "harddisk", "hardened", "hardener", "hardening", "hardhat", "hardhead", "hardiness", "hardly", "hardness", "hardship", "hardware", "hardwired", "hardwood", "hardy", "harmful", "harmless", "harmonica", "harmonics", "harmonize", "harmony", "harness", "harpist", "harsh", "harvest", "hash", "hassle", "haste", "hastily", "hastiness", "hasty", "hatbox", "hatchback", "hatchery", "hatchet", "hatching", "hatchling", "hate", "hatless", "hatred", "haunt", "haven", "hazard", "hazelnut", "hazily", "haziness", "hazing", "hazy", "headache", "headband", "headboard", "headcount", "headdress", "headed", "header", "headfirst", "headgear", "heading", "headlamp", "headless", "headlock", "headphone", "headpiece", "headrest", "headroom", "headscarf", "headset", "headsman", "headstand", "headstone", "headway", "headwear", "heap", "heat", "heave", "heavily", "heaviness", "heaving", "hedge", "hedging", "heftiness", "hefty", "helium", "helmet", "helper", "helpful", "helping", "helpless", "helpline", "hemlock", "hemstitch", "hence", "henchman", "henna", "herald", "herbal", "herbicide", "herbs", "heritage", "hermit", "heroics", "heroism", "herring", "herself", "hertz", "hesitancy", "hesitant", "hesitate", "hexagon", "hexagram", "hubcap", "huddle", "huddling", "huff", "hug", "hula", "hulk", "hull", "human", "humble", "humbling", "humbly", "humid", "humiliate", "humility", "humming", "hummus", "humongous", "humorist", "humorless", "humorous", "humpback", "humped", "humvee", "hunchback", "hundredth", "hunger", "hungrily", "hungry", "hunk", "hunter", "hunting", "huntress", "huntsman", "hurdle", "hurled", "hurler", "hurling", "hurray", "hurricane", "hurried", "hurry", "hurt", "husband", "hush", "husked", "huskiness", "hut", "hybrid", "hydrant", "hydrated", "hydration", "hydrogen", "hydroxide", "hyperlink", "hypertext", "hyphen", "hypnoses", "hypnosis", "hypnotic", "hypnotism", "hypnotist", "hypnotize", "hypocrisy", "hypocrite", "ibuprofen", "ice", "iciness", "icing", "icky", "icon", "icy", "idealism", "idealist", "idealize", "ideally", "idealness", "identical", "identify", "identity", "ideology", "idiocy", "idiom", "idly", "igloo", "ignition", "ignore", "iguana", "illicitly", "illusion", "illusive", "image", "imaginary", "imagines", "imaging", "imbecile", "imitate", "imitation", "immature", "immerse", "immersion", "imminent", "immobile", "immodest", "immorally", "immortal", "immovable", "immovably", "immunity", "immunize", "impaired", "impale", "impart", "impatient", "impeach", "impeding", "impending", "imperfect", "imperial", "impish", "implant", "implement", "implicate", "implicit", "implode", "implosion", "implosive", "imply", "impolite", "important", "importer", "impose", "imposing", "impotence", "impotency", "impotent", "impound", "imprecise", "imprint", "imprison", "impromptu", "improper", "improve", "improving", "improvise", "imprudent", "impulse", "impulsive", "impure", "impurity", "iodine", "iodize", "ion", "ipad", "iphone", "ipod", "irate", "irk", "iron", "irregular", "irrigate", "irritable", "irritably", "irritant", "irritate", "islamic", "islamist", "isolated", "isolating", "isolation", "isotope", "issue", "issuing", "italicize", "italics", "item", "itinerary", "itunes", "ivory", "ivy", "jab", "jackal", "jacket", "jackknife", "jackpot", "jailbird", "jailbreak", "jailer", "jailhouse", "jalapeno", "jam", "janitor", "january", "jargon", "jarring", "jasmine", "jaundice", "jaunt", "java", "jawed", "jawless", "jawline", "jaws", "jaybird", "jaywalker", "jazz", "jeep", "jeeringly", "jellied", "jelly", "jersey", "jester", "jet", "jiffy", "jigsaw", "jimmy", "jingle", "jingling", "jinx", "jitters", "jittery", "job", "jockey", "jockstrap", "jogger", "jogging", "john", "joining", "jokester", "jokingly", "jolliness", "jolly", "jolt", "jot", "jovial", "joyfully", "joylessly", "joyous", "joyride", "joystick", "jubilance", "jubilant", "judge", "judgingly", "judicial", "judiciary", "judo", "juggle", "juggling", "jugular", "juice", "juiciness", "juicy", "jujitsu", "jukebox", "july", "jumble", "jumbo", "jump", "junction", "juncture", "june", "junior", "juniper", "junkie", "junkman", "junkyard", "jurist", "juror", "jury", "justice", "justifier", "justify", "justly", "justness", "juvenile", "kabob", "kangaroo", "karaoke", "karate", "karma", "kebab", "keenly", "keenness", "keep", "keg", "kelp", "kennel", "kept", "kerchief", "kerosene", "kettle", "kick", "kiln", "kilobyte", "kilogram", "kilometer", "kilowatt", "kilt", "kimono", "kindle", "kindling", "kindly", "kindness", "kindred", "kinetic", "kinfolk", "king", "kinship", "kinsman", "kinswoman", "kissable", "kisser", "kissing", "kitchen", "kite", "kitten", "kitty", "kiwi", "kleenex", "knapsack", "knee", "knelt", "knickers", "knoll", "koala", "kooky", "kosher", "krypton", "kudos", "kung", "labored", "laborer", "laboring", "laborious", "labrador", "ladder", "ladies", "ladle", "ladybug", "ladylike", "lagged", "lagging", "lagoon", "lair", "lake", "lance", "landed", "landfall", "landfill", "landing", "landlady", "landless", "landline", "landlord", "landmark", "landmass", "landmine", "landowner", "landscape", "landside", "landslide", "language", "lankiness", "lanky", "lantern", "lapdog", "lapel", "lapped", "lapping", "laptop", "lard", "large", "lark", "lash", "lasso", "last", "latch", "late", "lather", "latitude", "latrine", "latter", "latticed", "launch", "launder", "laundry", "laurel", "lavender", "lavish", "laxative", "lazily", "laziness", "lazy", "lecturer", "left", "legacy", "legal", "legend", "legged", "leggings", "legible", "legibly", "legislate", "lego", "legroom", "legume", "legwarmer", "legwork", "lemon", "lend", "length", "lens", "lent", "leotard", "lesser", "letdown", "lethargic", "lethargy", "letter", "lettuce", "level", "leverage", "levers", "levitate", "levitator", "liability", "liable", "liberty", "librarian", "library", "licking", "licorice", "lid", "life", "lifter", "lifting", "liftoff", "ligament", "likely", "likeness", "likewise", "liking", "lilac", "lilly", "lily", "limb", "limeade", "limelight", "limes", "limit", "limping", "limpness", "line", "lingo", "linguini", "linguist", "lining", "linked", "linoleum", "linseed", "lint", "lion", "lip", "liquefy", "liqueur", "liquid", "lisp", "list", "litigate", "litigator", "litmus", "litter", "little", "livable", "lived", "lively", "liver", "livestock", "lividly", "living", "lizard", "lubricant", "lubricate", "lucid", "luckily", "luckiness", "luckless", "lucrative", "ludicrous", "lugged", "lukewarm", "lullaby", "lumber", "luminance", "luminous", "lumpiness", "lumping", "lumpish", "lunacy", "lunar", "lunchbox", "luncheon", "lunchroom", "lunchtime", "lung", "lurch", "lure", "luridness", "lurk", "lushly", "lushness", "luster", "lustfully", "lustily", "lustiness", "lustrous", "lusty", "luxurious", "luxury", "lying", "lyrically", "lyricism", "lyricist", "lyrics", "macarena", "macaroni", "macaw", "mace", "machine", "machinist", "magazine", "magenta", "maggot", "magical", "magician", "magma", "magnesium", "magnetic", "magnetism", "magnetize", "magnifier", "magnify", "magnitude", "magnolia", "mahogany", "maimed", "majestic", "majesty", "majorette", "majority", "makeover", "maker", "makeshift", "making", "malformed", "malt", "mama", "mammal", "mammary", "mammogram", "manager", "managing", "manatee", "mandarin", "mandate", "mandatory", "mandolin", "manger", "mangle", "mango", "mangy", "manhandle", "manhole", "manhood", "manhunt", "manicotti", "manicure", "manifesto", "manila", "mankind", "manlike", "manliness", "manly", "manmade", "manned", "mannish", "manor", "manpower", "mantis", "mantra", "manual", "many", "map", "marathon", "marauding", "marbled", "marbles", "marbling", "march", "mardi", "margarine", "margarita", "margin", "marigold", "marina", "marine", "marital", "maritime", "marlin", "marmalade", "maroon", "married", "marrow", "marry", "marshland", "marshy", "marsupial", "marvelous", "marxism", "mascot", "masculine", "mashed", "mashing", "massager", "masses", "massive", "mastiff", "matador", "matchbook", "matchbox", "matcher", "matching", "matchless", "material", "maternal", "maternity", "math", "mating", "matriarch", "matrimony", "matrix", "matron", "matted", "matter", "maturely", "maturing", "maturity", "mauve", "maverick", "maximize", "maximum", "maybe", "mayday", "mayflower", "moaner", "moaning", "mobile", "mobility", "mobilize", "mobster", "mocha", "mocker", "mockup", "modified", "modify", "modular", "modulator", "module", "moisten", "moistness", "moisture", "molar", "molasses", "mold", "molecular", "molecule", "molehill", "mollusk", "mom", "monastery", "monday", "monetary", "monetize", "moneybags", "moneyless", "moneywise", "mongoose", "mongrel", "monitor", "monkhood", "monogamy", "monogram", "monologue", "monopoly", "monorail", "monotone", "monotype", "monoxide", "monsieur", "monsoon", "monstrous", "monthly", "monument", "moocher", "moodiness", "moody", "mooing", "moonbeam", "mooned", "moonlight", "moonlike", "moonlit", "moonrise", "moonscape", "moonshine", "moonstone", "moonwalk", "mop", "morale", "morality", "morally", "morbidity", "morbidly", "morphine", "morphing", "morse", "mortality", "mortally", "mortician", "mortified", "mortify", "mortuary", "mosaic", "mossy", "most", "mothball", "mothproof", "motion", "motivate", "motivator", "motive", "motocross", "motor", "motto", "mountable", "mountain", "mounted", "mounting", "mourner", "mournful", "mouse", "mousiness", "moustache", "mousy", "mouth", "movable", "move", "movie", "moving", "mower", "mowing", "much", "muck", "mud", "mug", "mulberry", "mulch", "mule", "mulled", "mullets", "multiple", "multiply", "multitask", "multitude", "mumble", "mumbling", "mumbo", "mummified", "mummify", "mummy", "mumps", "munchkin", "mundane", "municipal", "muppet", "mural", "murkiness", "murky", "murmuring", "muscular", "museum", "mushily", "mushiness", "mushroom", "mushy", "music", "musket", "muskiness", "musky", "mustang", "mustard", "muster", "mustiness", "musty", "mutable", "mutate", "mutation", "mute", "mutilated", "mutilator", "mutiny", "mutt", "mutual", "muzzle", "myself", "myspace", "mystified", "mystify", "myth", "nacho", "nag", "nail", "name", "naming", "nanny", "nanometer", "nape", "napkin", "napped", "napping", "nappy", "narrow", "nastily", "nastiness", "national", "native", "nativity", "natural", "nature", "naturist", "nautical", "navigate", "navigator", "navy", "nearby", "nearest", "nearly", "nearness", "neatly", "neatness", "nebula", "nebulizer", "nectar", "negate", "negation", "negative", "neglector", "negligee", "negligent", "negotiate", "nemeses", "nemesis", "neon", "nephew", "nerd", "nervous", "nervy", "nest", "net", "neurology", "neuron", "neurosis", "neurotic", "neuter", "neutron", "never", "next", "nibble", "nickname", "nicotine", "niece", "nifty", "nimble", "nimbly", "nineteen", "ninetieth", "ninja", "nintendo", "ninth", "nuclear", "nuclei", "nucleus", "nugget", "nullify", "number", "numbing", "numbly", "numbness", "numeral", "numerate", "numerator", "numeric", "numerous", "nuptials", "nursery", "nursing", "nurture", "nutcase", "nutlike", "nutmeg", "nutrient", "nutshell", "nuttiness", "nutty", "nuzzle", "nylon", "oaf", "oak", "oasis", "oat", "obedience", "obedient", "obituary", "object", "obligate", "obliged", "oblivion", "oblivious", "oblong", "obnoxious", "oboe", "obscure", "obscurity", "observant", "observer", "observing", "obsessed", "obsession", "obsessive", "obsolete", "obstacle", "obstinate", "obstruct", "obtain", "obtrusive", "obtuse", "obvious", "occultist", "occupancy", "occupant", "occupier", "occupy", "ocean", "ocelot", "octagon", "octane", "october", "octopus", "ogle", "oil", "oink", "ointment", "okay", "old", "olive", "olympics", "omega", "omen", "ominous", "omission", "omit", "omnivore", "onboard", "oncoming", "ongoing", "onion", "online", "onlooker", "only", "onscreen", "onset", "onshore", "onslaught", "onstage", "onto", "onward", "onyx", "oops", "ooze", "oozy", "opacity", "opal", "open", "operable", "operate", "operating", "operation", "operative", "operator", "opium", "opossum", "opponent", "oppose", "opposing", "opposite", "oppressed", "oppressor", "opt", "opulently", "osmosis", "other", "otter", "ouch", "ought", "ounce", "outage", "outback", "outbid", "outboard", "outbound", "outbreak", "outburst", "outcast", "outclass", "outcome", "outdated", "outdoors", "outer", "outfield", "outfit", "outflank", "outgoing", "outgrow", "outhouse", "outing", "outlast", "outlet", "outline", "outlook", "outlying", "outmatch", "outmost", "outnumber", "outplayed", "outpost", "outpour", "output", "outrage", "outrank", "outreach", "outright", "outscore", "outsell", "outshine", "outshoot", "outsider", "outskirts", "outsmart", "outsource", "outspoken", "outtakes", "outthink", "outward", "outweigh", "outwit", "oval", "ovary", "oven", "overact", "overall", "overarch", "overbid", "overbill", "overbite", "overblown", "overboard", "overbook", "overbuilt", "overcast", "overcoat", "overcome", "overcook", "overcrowd", "overdraft", "overdrawn", "overdress", "overdrive", "overdue", "overeager", "overeater", "overexert", "overfed", "overfeed", "overfill", "overflow", "overfull", "overgrown", "overhand", "overhang", "overhaul", "overhead", "overhear", "overheat", "overhung", "overjoyed", "overkill", "overlabor", "overlaid", "overlap", "overlay", "overload", "overlook", "overlord", "overlying", "overnight", "overpass", "overpay", "overplant", "overplay", "overpower", "overprice", "overrate", "overreach", "overreact", "override", "overripe", "overrule", "overrun", "overshoot", "overshot", "oversight", "oversized", "oversleep", "oversold", "overspend", "overstate", "overstay", "overstep", "overstock", "overstuff", "oversweet", "overtake", "overthrow", "overtime", "overtly", "overtone", "overture", "overturn", "overuse", "overvalue", "overview", "overwrite", "owl", "oxford", "oxidant", "oxidation", "oxidize", "oxidizing", "oxygen", "oxymoron", "oyster", "ozone", "paced", "pacemaker", "pacific", "pacifier", "pacifism", "pacifist", "pacify", "padded", "padding", "paddle", "paddling", "padlock", "pagan", "pager", "paging", "pajamas", "palace", "palatable", "palm", "palpable", "palpitate", "paltry", "pampered", "pamperer", "pampers", "pamphlet", "panama", "pancake", "pancreas", "panda", "pandemic", "pang", "panhandle", "panic", "panning", "panorama", "panoramic", "panther", "pantomime", "pantry", "pants", "pantyhose", "paparazzi", "papaya", "paper", "paprika", "papyrus", "parabola", "parachute", "parade", "paradox", "paragraph", "parakeet", "paralegal", "paralyses", "paralysis", "paralyze", "paramedic", "parameter", "paramount", "parasail", "parasite", "parasitic", "parcel", "parched", "parchment", "pardon", "parish", "parka", "parking", "parkway", "parlor", "parmesan", "parole", "parrot", "parsley", "parsnip", "partake", "parted", "parting", "partition", "partly", "partner", "partridge", "party", "passable", "passably", "passage", "passcode", "passenger", "passerby", "passing", "passion", "passive", "passivism", "passover", "passport", "password", "pasta", "pasted", "pastel", "pastime", "pastor", "pastrami", "pasture", "pasty", "patchwork", "patchy", "paternal", "paternity", "path", "patience", "patient", "patio", "patriarch", "patriot", "patrol", "patronage", "patronize", "pauper", "pavement", "paver", "pavestone", "pavilion", "paving", "pawing", "payable", "payback", "paycheck", "payday", "payee", "payer", "paying", "payment", "payphone", "payroll", "pebble", "pebbly", "pecan", "pectin", "peculiar", "peddling", "pediatric", "pedicure", "pedigree", "pedometer", "pegboard", "pelican", "pellet", "pelt", "pelvis", "penalize", "penalty", "pencil", "pendant", "pending", "penholder", "penknife", "pennant", "penniless", "penny", "penpal", "pension", "pentagon", "pentagram", "pep", "perceive", "percent", "perch", "percolate", "perennial", "perfected", "perfectly", "perfume", "periscope", "perish", "perjurer", "perjury", "perkiness", "perky", "perm", "peroxide", "perpetual", "perplexed", "persecute", "persevere", "persuaded", "persuader", "pesky", "peso", "pessimism", "pessimist", "pester", "pesticide", "petal", "petite", "petition", "petri", "petroleum", "petted", "petticoat", "pettiness", "petty", "petunia", "phantom", "phobia", "phoenix", "phonebook", "phoney", "phonics", "phoniness", "phony", "phosphate", "photo", "phrase", "phrasing", "placard", "placate", "placidly", "plank", "planner", "plant", "plasma", "plaster", "plastic", "plated", "platform", "plating", "platinum", "platonic", "platter", "platypus", "plausible", "plausibly", "playable", "playback", "player", "playful", "playgroup", "playhouse", "playing", "playlist", "playmaker", "playmate", "playoff", "playpen", "playroom", "playset", "plaything", "playtime", "plaza", "pleading", "pleat", "pledge", "plentiful", "plenty", "plethora", "plexiglas", "pliable", "plod", "plop", "plot", "plow", "ploy", "pluck", "plug", "plunder", "plunging", "plural", "plus", "plutonium", "plywood", "poach", "pod", "poem", "poet", "pogo", "pointed", "pointer", "pointing", "pointless", "pointy", "poise", "poison", "poker", "poking", "polar", "police", "policy", "polio", "polish", "politely", "polka", "polo", "polyester", "polygon", "polygraph", "polymer", "poncho", "pond", "pony", "popcorn", "pope", "poplar", "popper", "poppy", "popsicle", "populace", "popular", "populate", "porcupine", "pork", "porous", "porridge", "portable", "portal", "portfolio", "porthole", "portion", "portly", "portside", "poser", "posh", "posing", "possible", "possibly", "possum", "postage", "postal", "postbox", "postcard", "posted", "poster", "posting", "postnasal", "posture", "postwar", "pouch", "pounce", "pouncing", "pound", "pouring", "pout", "powdered", "powdering", "powdery", "power", "powwow", "pox", "praising", "prance", "prancing", "pranker", "prankish", "prankster", "prayer", "praying", "preacher", "preaching", "preachy", "preamble", "precinct", "precise", "precision", "precook", "precut", "predator", "predefine", "predict", "preface", "prefix", "preflight", "preformed", "pregame", "pregnancy", "pregnant", "preheated", "prelaunch", "prelaw", "prelude", "premiere", "premises", "premium", "prenatal", "preoccupy", "preorder", "prepaid", "prepay", "preplan", "preppy", "preschool", "prescribe", "preseason", "preset", "preshow", "president", "presoak", "press", "presume", "presuming", "preteen", "pretended", "pretender", "pretense", "pretext", "pretty", "pretzel", "prevail", "prevalent", "prevent", "preview", "previous", "prewar", "prewashed", "prideful", "pried", "primal", "primarily", "primary", "primate", "primer", "primp", "princess", "print", "prior", "prism", "prison", "prissy", "pristine", "privacy", "private", "privatize", "prize", "proactive", "probable", "probably", "probation", "probe", "probing", "probiotic", "problem", "procedure", "process", "proclaim", "procreate", "procurer", "prodigal", "prodigy", "produce", "product", "profane", "profanity", "professed", "professor", "profile", "profound", "profusely", "progeny", "prognosis", "program", "progress", "projector", "prologue", "prolonged", "promenade", "prominent", "promoter", "promotion", "prompter", "promptly", "prone", "prong", "pronounce", "pronto", "proofing", "proofread", "proofs", "propeller", "properly", "property", "proponent", "proposal", "propose", "props", "prorate", "protector", "protegee", "proton", "prototype", "protozoan", "protract", "protrude", "proud", "provable", "proved", "proven", "provided", "provider", "providing", "province", "proving", "provoke", "provoking", "provolone", "prowess", "prowler", "prowling", "proximity", "proxy", "prozac", "prude", "prudishly", "prune", "pruning", "pry", "psychic", "public", "publisher", "pucker", "pueblo", "pug", "pull", "pulmonary", "pulp", "pulsate", "pulse", "pulverize", "puma", "pumice", "pummel", "punch", "punctual", "punctuate", "punctured", "pungent", "punisher", "punk", "pupil", "puppet", "puppy", "purchase", "pureblood", "purebred", "purely", "pureness", "purgatory", "purge", "purging", "purifier", "purify", "purist", "puritan", "purity", "purple", "purplish", "purposely", "purr", "purse", "pursuable", "pursuant", "pursuit", "purveyor", "pushcart", "pushchair", "pusher", "pushiness", "pushing", "pushover", "pushpin", "pushup", "pushy", "putdown", "putt", "puzzle", "puzzling", "pyramid", "pyromania", "python", "quack", "quadrant", "quail", "quaintly", "quake", "quaking", "qualified", "qualifier", "qualify", "quality", "qualm", "quantum", "quarrel", "quarry", "quartered", "quarterly", "quarters", "quartet", "quench", "query", "quicken", "quickly", "quickness", "quicksand", "quickstep", "quiet", "quill", "quilt", "quintet", "quintuple", "quirk", "quit", "quiver", "quizzical", "quotable", "quotation", "quote", "rabid", "race", "racing", "racism", "rack", "racoon", "radar", "radial", "radiance", "radiantly", "radiated", "radiation", "radiator", "radio", "radish", "raffle", "raft", "rage", "ragged", "raging", "ragweed", "raider", "railcar", "railing", "railroad", "railway", "raisin", "rake", "raking", "rally", "ramble", "rambling", "ramp", "ramrod", "ranch", "rancidity", "random", "ranged", "ranger", "ranging", "ranked", "ranking", "ransack", "ranting", "rants", "rare", "rarity", "rascal", "rash", "rasping", "ravage", "raven", "ravine", "raving", "ravioli", "ravishing", "reabsorb", "reach", "reacquire", "reaction", "reactive", "reactor", "reaffirm", "ream", "reanalyze", "reappear", "reapply", "reappoint", "reapprove", "rearrange", "rearview", "reason", "reassign", "reassure", "reattach", "reawake", "rebalance", "rebate", "rebel", "rebirth", "reboot", "reborn", "rebound", "rebuff", "rebuild", "rebuilt", "reburial", "rebuttal", "recall", "recant", "recapture", "recast", "recede", "recent", "recess", "recharger", "recipient", "recital", "recite", "reckless", "reclaim", "recliner", "reclining", "recluse", "reclusive", "recognize", "recoil", "recollect", "recolor", "reconcile", "reconfirm", "reconvene", "recopy", "record", "recount", "recoup", "recovery", "recreate", "rectal", "rectangle", "rectified", "rectify", "recycled", "recycler", "recycling", "reemerge", "reenact", "reenter", "reentry", "reexamine", "referable", "referee", "reference", "refill", "refinance", "refined", "refinery", "refining", "refinish", "reflected", "reflector", "reflex", "reflux", "refocus", "refold", "reforest", "reformat", "reformed", "reformer", "reformist", "refract", "refrain", "refreeze", "refresh", "refried", "refueling", "refund", "refurbish", "refurnish", "refusal", "refuse", "refusing", "refutable", "refute", "regain", "regalia", "regally", "reggae", "regime", "region", "register", "registrar", "registry", "regress", "regretful", "regroup", "regular", "regulate", "regulator", "rehab", "reheat", "rehire", "rehydrate", "reimburse", "reissue", "reiterate", "rejoice", "rejoicing", "rejoin", "rekindle", "relapse", "relapsing", "relatable", "related", "relation", "relative", "relax", "relay", "relearn", "release", "relenting", "reliable", "reliably", "reliance", "reliant", "relic", "relieve", "relieving", "relight", "relish", "relive", "reload", "relocate", "relock", "reluctant", "rely", "remake", "remark", "remarry", "rematch", "remedial", "remedy", "remember", "reminder", "remindful", "remission", "remix", "remnant", "remodeler", "remold", "remorse", "remote", "removable", "removal", "removed", "remover", "removing", "rename", "renderer", "rendering", "rendition", "renegade", "renewable", "renewably", "renewal", "renewed", "renounce", "renovate", "renovator", "rentable", "rental", "rented", "renter", "reoccupy", "reoccur", "reopen", "reorder", "repackage", "repacking", "repaint", "repair", "repave", "repaying", "repayment", "repeal", "repeated", "repeater", "repent", "rephrase", "replace", "replay", "replica", "reply", "reporter", "repose", "repossess", "repost", "repressed", "reprimand", "reprint", "reprise", "reproach", "reprocess", "reproduce", "reprogram", "reps", "reptile", "reptilian", "repugnant", "repulsion", "repulsive", "repurpose", "reputable", "reputably", "request", "require", "requisite", "reroute", "rerun", "resale", "resample", "rescuer", "reseal", "research", "reselect", "reseller", "resemble", "resend", "resent", "reset", "reshape", "reshoot", "reshuffle", "residence", "residency", "resident", "residual", "residue", "resigned", "resilient", "resistant", "resisting", "resize", "resolute", "resolved", "resonant", "resonate", "resort", "resource", "respect", "resubmit", "result", "resume", "resupply", "resurface", "resurrect", "retail", "retainer", "retaining", "retake", "retaliate", "retention", "rethink", "retinal", "retired", "retiree", "retiring", "retold", "retool", "retorted", "retouch", "retrace", "retract", "retrain", "retread", "retreat", "retrial", "retrieval", "retriever", "retry", "return", "retying", "retype", "reunion", "reunite", "reusable", "reuse", "reveal", "reveler", "revenge", "revenue", "reverb", "revered", "reverence", "reverend", "reversal", "reverse", "reversing", "reversion", "revert", "revisable", "revise", "revision", "revisit", "revivable", "revival", "reviver", "reviving", "revocable", "revoke", "revolt", "revolver", "revolving", "reward", "rewash", "rewind", "rewire", "reword", "rework", "rewrap", "rewrite", "rhyme", "ribbon", "ribcage", "rice", "riches", "richly", "richness", "rickety", "ricotta", "riddance", "ridden", "ride", "riding", "rifling", "rift", "rigging", "rigid", "rigor", "rimless", "rimmed", "rind", "rink", "rinse", "rinsing", "riot", "ripcord", "ripeness", "ripening", "ripping", "ripple", "rippling", "riptide", "rise", "rising", "risk", "risotto", "ritalin", "ritzy", "rival", "riverbank", "riverbed", "riverboat", "riverside", "riveter", "riveting", "roamer", "roaming", "roast", "robbing", "robe", "robin", "robotics", "robust", "rockband", "rocker", "rocket", "rockfish", "rockiness", "rocking", "rocklike", "rockslide", "rockstar", "rocky", "rogue", "roman", "romp", "rope", "roping", "roster", "rosy", "rotten", "rotting", "rotunda", "roulette", "rounding", "roundish", "roundness", "roundup", "roundworm", "routine", "routing", "rover", "roving", "royal", "rubbed", "rubber", "rubbing", "rubble", "rubdown", "ruby", "ruckus", "rudder", "rug", "ruined", "rule", "rumble", "rumbling", "rummage", "rumor", "runaround", "rundown", "runner", "running", "runny", "runt", "runway", "rupture", "rural", "ruse", "rush", "rust", "rut", "sabbath", "sabotage", "sacrament", "sacred", "sacrifice", "sadden", "saddlebag", "saddled", "saddling", "sadly", "sadness", "safari", "safeguard", "safehouse", "safely", "safeness", "saffron", "saga", "sage", "sagging", "saggy", "said", "saint", "sake", "salad", "salami", "salaried", "salary", "saline", "salon", "saloon", "salsa", "salt", "salutary", "salute", "salvage", "salvaging", "salvation", "same", "sample", "sampling", "sanction", "sanctity", "sanctuary", "sandal", "sandbag", "sandbank", "sandbar", "sandblast", "sandbox", "sanded", "sandfish", "sanding", "sandlot", "sandpaper", "sandpit", "sandstone", "sandstorm", "sandworm", "sandy", "sanitary", "sanitizer", "sank", "santa", "sapling", "sappiness", "sappy", "sarcasm", "sarcastic", "sardine", "sash", "sasquatch", "sassy", "satchel", "satiable", "satin", "satirical", "satisfied", "satisfy", "saturate", "saturday", "sauciness", "saucy", "sauna", "savage", "savanna", "saved", "savings", "savior", "savor", "saxophone", "say", "scabbed", "scabby", "scalded", "scalding", "scale", "scaling", "scallion", "scallop", "scalping", "scam", "scandal", "scanner", "scanning", "scant", "scapegoat", "scarce", "scarcity", "scarecrow", "scared", "scarf", "scarily", "scariness", "scarring", "scary", "scavenger", "scenic", "schedule", "schematic", "scheme", "scheming", "schilling", "schnapps", "scholar", "science", "scientist", "scion", "scoff", "scolding", "scone", "scoop", "scooter", "scope", "scorch", "scorebook", "scorecard", "scored", "scoreless", "scorer", "scoring", "scorn", "scorpion", "scotch", "scoundrel", "scoured", "scouring", "scouting", "scouts", "scowling", "scrabble", "scraggly", "scrambled", "scrambler", "scrap", "scratch", "scrawny", "screen", "scribble", "scribe", "scribing", "scrimmage", "script", "scroll", "scrooge", "scrounger", "scrubbed", "scrubber", "scruffy", "scrunch", "scrutiny", "scuba", "scuff", "sculptor", "sculpture", "scurvy", "scuttle", "secluded", "secluding", "seclusion", "second", "secrecy", "secret", "sectional", "sector", "secular", "securely", "security", "sedan", "sedate", "sedation", "sedative", "sediment", "seduce", "seducing", "segment", "seismic", "seizing", "seldom", "selected", "selection", "selective", "selector", "self", "seltzer", "semantic", "semester", "semicolon", "semifinal", "seminar", "semisoft", "semisweet", "senate", "senator", "send", "senior", "senorita", "sensation", "sensitive", "sensitize", "sensually", "sensuous", "sepia", "september", "septic", "septum", "sequel", "sequence", "sequester", "series", "sermon", "serotonin", "serpent", "serrated", "serve", "service", "serving", "sesame", "sessions", "setback", "setting", "settle", "settling", "setup", "sevenfold", "seventeen", "seventh", "seventy", "severity", "shabby", "shack", "shaded", "shadily", "shadiness", "shading", "shadow", "shady", "shaft", "shakable", "shakily", "shakiness", "shaking", "shaky", "shale", "shallot", "shallow", "shame", "shampoo", "shamrock", "shank", "shanty", "shape", "shaping", "share", "sharpener", "sharper", "sharpie", "sharply", "sharpness", "shawl", "sheath", "shed", "sheep", "sheet", "shelf", "shell", "shelter", "shelve", "shelving", "sherry", "shield", "shifter", "shifting", "shiftless", "shifty", "shimmer", "shimmy", "shindig", "shine", "shingle", "shininess", "shining", "shiny", "ship", "shirt", "shivering", "shock", "shone", "shoplift", "shopper", "shopping", "shoptalk", "shore", "shortage", "shortcake", "shortcut", "shorten", "shorter", "shorthand", "shortlist", "shortly", "shortness", "shorts", "shortwave", "shorty", "shout", "shove", "showbiz", "showcase", "showdown", "shower", "showgirl", "showing", "showman", "shown", "showoff", "showpiece", "showplace", "showroom", "showy", "shrank", "shrapnel", "shredder", "shredding", "shrewdly", "shriek", "shrill", "shrimp", "shrine", "shrink", "shrivel", "shrouded", "shrubbery", "shrubs", "shrug", "shrunk", "shucking", "shudder", "shuffle", "shuffling", "shun", "shush", "shut", "shy", "siamese", "siberian", "sibling", "siding", "sierra", "siesta", "sift", "sighing", "silenced", "silencer", "silent", "silica", "silicon", "silk", "silliness", "silly", "silo", "silt", "silver", "similarly", "simile", "simmering", "simple", "simplify", "simply", "sincere", "sincerity", "singer", "singing", "single", "singular", "sinister", "sinless", "sinner", "sinuous", "sip", "siren", "sister", "sitcom", "sitter", "sitting", "situated", "situation", "sixfold", "sixteen", "sixth", "sixties", "sixtieth", "sixtyfold", "sizable", "sizably", "size", "sizing", "sizzle", "sizzling", "skater", "skating", "skedaddle", "skeletal", "skeleton", "skeptic", "sketch", "skewed", "skewer", "skid", "skied", "skier", "skies", "skiing", "skilled", "skillet", "skillful", "skimmed", "skimmer", "skimming", "skimpily", "skincare", "skinhead", "skinless", "skinning", "skinny", "skintight", "skipper", "skipping", "skirmish", "skirt", "skittle", "skydiver", "skylight", "skyline", "skype", "skyrocket", "skyward", "slab", "slacked", "slacker", "slacking", "slackness", "slacks", "slain", "slam", "slander", "slang", "slapping", "slapstick", "slashed", "slashing", "slate", "slather", "slaw", "sled", "sleek", "sleep", "sleet", "sleeve", "slept", "sliceable", "sliced", "slicer", "slicing", "slick", "slider", "slideshow", "sliding", "slighted", "slighting", "slightly", "slimness", "slimy", "slinging", "slingshot", "slinky", "slip", "slit", "sliver", "slobbery", "slogan", "sloped", "sloping", "sloppily", "sloppy", "slot", "slouching", "slouchy", "sludge", "slug", "slum", "slurp", "slush", "sly", "small", "smartly", "smartness", "smasher", "smashing", "smashup", "smell", "smelting", "smile", "smilingly", "smirk", "smite", "smith", "smitten", "smock", "smog", "smoked", "smokeless", "smokiness", "smoking", "smoky", "smolder", "smooth", "smother", "smudge", "smudgy", "smuggler", "smuggling", "smugly", "smugness", "snack", "snagged", "snaking", "snap", "snare", "snarl", "snazzy", "sneak", "sneer", "sneeze", "sneezing", "snide", "sniff", "snippet", "snipping", "snitch", "snooper", "snooze", "snore", "snoring", "snorkel", "snort", "snout", "snowbird", "snowboard", "snowbound", "snowcap", "snowdrift", "snowdrop", "snowfall", "snowfield", "snowflake", "snowiness", "snowless", "snowman", "snowplow", "snowshoe", "snowstorm", "snowsuit", "snowy", "snub", "snuff", "snuggle", "snugly", "snugness", "speak", "spearfish", "spearhead", "spearman", "spearmint", "species", "specimen", "specked", "speckled", "specks", "spectacle", "spectator", "spectrum", "speculate", "speech", "speed", "spellbind", "speller", "spelling", "spendable", "spender", "spending", "spent", "spew", "sphere", "spherical", "sphinx", "spider", "spied", "spiffy", "spill", "spilt", "spinach", "spinal", "spindle", "spinner", "spinning", "spinout", "spinster", "spiny", "spiral", "spirited", "spiritism", "spirits", "spiritual", "splashed", "splashing", "splashy", "splatter", "spleen", "splendid", "splendor", "splice", "splicing", "splinter", "splotchy", "splurge", "spoilage", "spoiled", "spoiler", "spoiling", "spoils", "spoken", "spokesman", "sponge", "spongy", "sponsor", "spoof", "spookily", "spooky", "spool", "spoon", "spore", "sporting", "sports", "sporty", "spotless", "spotlight", "spotted", "spotter", "spotting", "spotty", "spousal", "spouse", "spout", "sprain", "sprang", "sprawl", "spray", "spree", "sprig", "spring", "sprinkled", "sprinkler", "sprint", "sprite", "sprout", "spruce", "sprung", "spry", "spud", "spur", "sputter", "spyglass", "squabble", "squad", "squall", "squander", "squash", "squatted", "squatter", "squatting", "squeak", "squealer", "squealing", "squeamish", "squeegee", "squeeze", "squeezing", "squid", "squiggle", "squiggly", "squint", "squire", "squirt", "squishier", "squishy", "stability", "stabilize", "stable", "stack", "stadium", "staff", "stage", "staging", "stagnant", "stagnate", "stainable", "stained", "staining", "stainless", "stalemate", "staleness", "stalling", "stallion", "stamina", "stammer", "stamp", "stand", "stank", "staple", "stapling", "starboard", "starch", "stardom", "stardust", "starfish", "stargazer", "staring", "stark", "starless", "starlet", "starlight", "starlit", "starring", "starry", "starship", "starter", "starting", "startle", "startling", "startup", "starved", "starving", "stash", "state", "static", "statistic", "statue", "stature", "status", "statute", "statutory", "staunch", "stays", "steadfast", "steadier", "steadily", "steadying", "steam", "steed", "steep", "steerable", "steering", "steersman", "stegosaur", "stellar", "stem", "stench", "stencil", "step", "stereo", "sterile", "sterility", "sterilize", "sterling", "sternness", "sternum", "stew", "stick", "stiffen", "stiffly", "stiffness", "stifle", "stifling", "stillness", "stilt", "stimulant", "stimulate", "stimuli", "stimulus", "stinger", "stingily", "stinging", "stingray", "stingy", "stinking", "stinky", "stipend", "stipulate", "stir", "stitch", "stock", "stoic", "stoke", "stole", "stomp", "stonewall", "stoneware", "stonework", "stoning", "stony", "stood", "stooge", "stool", "stoop", "stoplight", "stoppable", "stoppage", "stopped", "stopper", "stopping", "stopwatch", "storable", "storage", "storeroom", "storewide", "storm", "stout", "stove", "stowaway", "stowing", "straddle", "straggler", "strained", "strainer", "straining", "strangely", "stranger", "strangle", "strategic", "strategy", "stratus", "straw", "stray", "streak", "stream", "street", "strength", "strenuous", "strep", "stress", "stretch", "strewn", "stricken", "strict", "stride", "strife", "strike", "striking", "strive", "striving", "strobe", "strode", "stroller", "strongbox", "strongly", "strongman", "struck", "structure", "strudel", "struggle", "strum", "strung", "strut", "stubbed", "stubble", "stubbly", "stubborn", "stucco", "stuck", "student", "studied", "studio", "study", "stuffed", "stuffing", "stuffy", "stumble", "stumbling", "stump", "stung", "stunned", "stunner", "stunning", "stunt", "stupor", "sturdily", "sturdy", "styling", "stylishly", "stylist", "stylized", "stylus", "suave", "subarctic", "subatomic", "subdivide", "subdued", "subduing", "subfloor", "subgroup", "subheader", "subject", "sublease", "sublet", "sublevel", "sublime", "submarine", "submerge", "submersed", "submitter", "subpanel", "subpar", "subplot", "subprime", "subscribe", "subscript", "subsector", "subside", "subsiding", "subsidize", "subsidy", "subsoil", "subsonic", "substance", "subsystem", "subtext", "subtitle", "subtly", "subtotal", "subtract", "subtype", "suburb", "subway", "subwoofer", "subzero", "succulent", "such", "suction", "sudden", "sudoku", "suds", "sufferer", "suffering", "suffice", "suffix", "suffocate", "suffrage", "sugar", "suggest", "suing", "suitable", "suitably", "suitcase", "suitor", "sulfate", "sulfide", "sulfite", "sulfur", "sulk", "sullen", "sulphate", "sulphuric", "sultry", "superbowl", "superglue", "superhero", "superior", "superjet", "superman", "supermom", "supernova", "supervise", "supper", "supplier", "supply", "support", "supremacy", "supreme", "surcharge", "surely", "sureness", "surface", "surfacing", "surfboard", "surfer", "surgery", "surgical", "surging", "surname", "surpass", "surplus", "surprise", "surreal", "surrender", "surrogate", "surround", "survey", "survival", "survive", "surviving", "survivor", "sushi", "suspect", "suspend", "suspense", "sustained", "sustainer", "swab", "swaddling", "swagger", "swampland", "swan", "swapping", "swarm", "sway", "swear", "sweat", "sweep", "swell", "swept", "swerve", "swifter", "swiftly", "swiftness", "swimmable", "swimmer", "swimming", "swimsuit", "swimwear", "swinger", "swinging", "swipe", "swirl", "switch", "swivel", "swizzle", "swooned", "swoop", "swoosh", "swore", "sworn", "swung", "sycamore", "sympathy", "symphonic", "symphony", "symptom", "synapse", "syndrome", "synergy", "synopses", "synopsis", "synthesis", "synthetic", "syrup", "system", "t-shirt", "tabasco", "tabby", "tableful", "tables", "tablet", "tableware", "tabloid", "tackiness", "tacking", "tackle", "tackling", "tacky", "taco", "tactful", "tactical", "tactics", "tactile", "tactless", "tadpole", "taekwondo", "tag", "tainted", "take", "taking", "talcum", "talisman", "tall", "talon", "tamale", "tameness", "tamer", "tamper", "tank", "tanned", "tannery", "tanning", "tantrum", "tapeless", "tapered", "tapering", "tapestry", "tapioca", "tapping", "taps", "tarantula", "target", "tarmac", "tarnish", "tarot", "tartar", "tartly", "tartness", "task", "tassel", "taste", "tastiness", "tasting", "tasty", "tattered", "tattle", "tattling", "tattoo", "taunt", "tavern", "thank", "that", "thaw", "theater", "theatrics", "thee", "theft", "theme", "theology", "theorize", "thermal", "thermos", "thesaurus", "these", "thesis", "thespian", "thicken", "thicket", "thickness", "thieving", "thievish", "thigh", "thimble", "thing", "think", "thinly", "thinner", "thinness", "thinning", "thirstily", "thirsting", "thirsty", "thirteen", "thirty", "thong", "thorn", "those", "thousand", "thrash", "thread", "threaten", "threefold", "thrift", "thrill", "thrive", "thriving", "throat", "throbbing", "throng", "throttle", "throwaway", "throwback", "thrower", "throwing", "thud", "thumb", "thumping", "thursday", "thus", "thwarting", "thyself", "tiara", "tibia", "tidal", "tidbit", "tidiness", "tidings", "tidy", "tiger", "tighten", "tightly", "tightness", "tightrope", "tightwad", "tigress", "tile", "tiling", "till", "tilt", "timid", "timing", "timothy", "tinderbox", "tinfoil", "tingle", "tingling", "tingly", "tinker", "tinkling", "tinsel", "tinsmith", "tint", "tinwork", "tiny", "tipoff", "tipped", "tipper", "tipping", "tiptoeing", "tiptop", "tiring", "tissue", "trace", "tracing", "track", "traction", "tractor", "trade", "trading", "tradition", "traffic", "tragedy", "trailing", "trailside", "train", "traitor", "trance", "tranquil", "transfer", "transform", "translate", "transpire", "transport", "transpose", "trapdoor", "trapeze", "trapezoid", "trapped", "trapper", "trapping", "traps", "trash", "travel", "traverse", "travesty", "tray", "treachery", "treading", "treadmill", "treason", "treat", "treble", "tree", "trekker", "tremble", "trembling", "tremor", "trench", "trend", "trespass", "triage", "trial", "triangle", "tribesman", "tribunal", "tribune", "tributary", "tribute", "triceps", "trickery", "trickily", "tricking", "trickle", "trickster", "tricky", "tricolor", "tricycle", "trident", "tried", "trifle", "trifocals", "trillion", "trilogy", "trimester", "trimmer", "trimming", "trimness", "trinity", "trio", "tripod", "tripping", "triumph", "trivial", "trodden", "trolling", "trombone", "trophy", "tropical", "tropics", "trouble", "troubling", "trough", "trousers", "trout", "trowel", "truce", "truck", "truffle", "trump", "trunks", "trustable", "trustee", "trustful", "trusting", "trustless", "truth", "try", "tubby", "tubeless", "tubular", "tucking", "tuesday", "tug", "tuition", "tulip", "tumble", "tumbling", "tummy", "turban", "turbine", "turbofan", "turbojet", "turbulent", "turf", "turkey", "turmoil", "turret", "turtle", "tusk", "tutor", "tutu", "tux", "tweak", "tweed", "tweet", "tweezers", "twelve", "twentieth", "twenty", "twerp", "twice", "twiddle", "twiddling", "twig", "twilight", "twine", "twins", "twirl", "twistable", "twisted", "twister", "twisting", "twisty", "twitch", "twitter", "tycoon", "tying", "tyke", "udder", "ultimate", "ultimatum", "ultra", "umbilical", "umbrella", "umpire", "unabashed", "unable", "unadorned", "unadvised", "unafraid", "unaired", "unaligned", "unaltered", "unarmored", "unashamed", "unaudited", "unawake", "unaware", "unbaked", "unbalance", "unbeaten", "unbend", "unbent", "unbiased", "unbitten", "unblended", "unblessed", "unblock", "unbolted", "unbounded", "unboxed", "unbraided", "unbridle", "unbroken", "unbuckled", "unbundle", "unburned", "unbutton", "uncanny", "uncapped", "uncaring", "uncertain", "unchain", "unchanged", "uncharted", "uncheck", "uncivil", "unclad", "unclaimed", "unclamped", "unclasp", "uncle", "unclip", "uncloak", "unclog", "unclothed", "uncoated", "uncoiled", "uncolored", "uncombed", "uncommon", "uncooked", "uncork", "uncorrupt", "uncounted", "uncouple", "uncouth", "uncover", "uncross", "uncrown", "uncrushed", "uncured", "uncurious", "uncurled", "uncut", "undamaged", "undated", "undaunted", "undead", "undecided", "undefined", "underage", "underarm", "undercoat", "undercook", "undercut", "underdog", "underdone", "underfed", "underfeed", "underfoot", "undergo", "undergrad", "underhand", "underline", "underling", "undermine", "undermost", "underpaid", "underpass", "underpay", "underrate", "undertake", "undertone", "undertook", "undertow", "underuse", "underwear", "underwent", "underwire", "undesired", "undiluted", "undivided", "undocked", "undoing", "undone", "undrafted", "undress", "undrilled", "undusted", "undying", "unearned", "unearth", "unease", "uneasily", "uneasy", "uneatable", "uneaten", "unedited", "unelected", "unending", "unengaged", "unenvied", "unequal", "unethical", "uneven", "unexpired", "unexposed", "unfailing", "unfair", "unfasten", "unfazed", "unfeeling", "unfiled", "unfilled", "unfitted", "unfitting", "unfixable", "unfixed", "unflawed", "unfocused", "unfold", "unfounded", "unframed", "unfreeze", "unfrosted", "unfrozen", "unfunded", "unglazed", "ungloved", "unglue", "ungodly", "ungraded", "ungreased", "unguarded", "unguided", "unhappily", "unhappy", "unharmed", "unhealthy", "unheard", "unhearing", "unheated", "unhelpful", "unhidden", "unhinge", "unhitched", "unholy", "unhook", "unicorn", "unicycle", "unified", "unifier", "uniformed", "uniformly", "unify", "unimpeded", "uninjured", "uninstall", "uninsured", "uninvited", "union", "uniquely", "unisexual", "unison", "unissued", "unit", "universal", "universe", "unjustly", "unkempt", "unkind", "unknotted", "unknowing", "unknown", "unlaced", "unlatch", "unlawful", "unleaded", "unlearned", "unleash", "unless", "unleveled", "unlighted", "unlikable", "unlimited", "unlined", "unlinked", "unlisted", "unlit", "unlivable", "unloaded", "unloader", "unlocked", "unlocking", "unlovable", "unloved", "unlovely", "unloving", "unluckily", "unlucky", "unmade", "unmanaged", "unmanned", "unmapped", "unmarked", "unmasked", "unmasking", "unmatched", "unmindful", "unmixable", "unmixed", "unmolded", "unmoral", "unmovable", "unmoved", "unmoving", "unnamable", "unnamed", "unnatural", "unneeded", "unnerve", "unnerving", "unnoticed", "unopened", "unopposed", "unpack", "unpadded", "unpaid", "unpainted", "unpaired", "unpaved", "unpeeled", "unpicked", "unpiloted", "unpinned", "unplanned", "unplanted", "unpleased", "unpledged", "unplowed", "unplug", "unpopular", "unproven", "unquote", "unranked", "unrated", "unraveled", "unreached", "unread", "unreal", "unreeling", "unrefined", "unrelated", "unrented", "unrest", "unretired", "unrevised", "unrigged", "unripe", "unrivaled", "unroasted", "unrobed", "unroll", "unruffled", "unruly", "unrushed", "unsaddle", "unsafe", "unsaid", "unsalted", "unsaved", "unsavory", "unscathed", "unscented", "unscrew", "unsealed", "unseated", "unsecured", "unseeing", "unseemly", "unseen", "unselect", "unselfish", "unsent", "unsettled", "unshackle", "unshaken", "unshaved", "unshaven", "unsheathe", "unshipped", "unsightly", "unsigned", "unskilled", "unsliced", "unsmooth", "unsnap", "unsocial", "unsoiled", "unsold", "unsolved", "unsorted", "unspoiled", "unspoken", "unstable", "unstaffed", "unstamped", "unsteady", "unsterile", "unstirred", "unstitch", "unstopped", "unstuck", "unstuffed", "unstylish", "unsubtle", "unsubtly", "unsuited", "unsure", "unsworn", "untagged", "untainted", "untaken", "untamed", "untangled", "untapped", "untaxed", "unthawed", "unthread", "untidy", "untie", "until", "untimed", "untimely", "untitled", "untoasted", "untold", "untouched", "untracked", "untrained", "untreated", "untried", "untrimmed", "untrue", "untruth", "unturned", "untwist", "untying", "unusable", "unused", "unusual", "unvalued", "unvaried", "unvarying", "unveiled", "unveiling", "unvented", "unviable", "unvisited", "unvocal", "unwanted", "unwarlike", "unwary", "unwashed", "unwatched", "unweave", "unwed", "unwelcome", "unwell", "unwieldy", "unwilling", "unwind", "unwired", "unwitting", "unwomanly", "unworldly", "unworn", "unworried", "unworthy", "unwound", "unwoven", "unwrapped", "unwritten", "unzip", "upbeat", "upchuck", "upcoming", "upcountry", "update", "upfront", "upgrade", "upheaval", "upheld", "uphill", "uphold", "uplifted", "uplifting", "upload", "upon", "upper", "upright", "uprising", "upriver", "uproar", "uproot", "upscale", "upside", "upstage", "upstairs", "upstart", "upstate", "upstream", "upstroke", "upswing", "uptake", "uptight", "uptown", "upturned", "upward", "upwind", "uranium", "urban", "urchin", "urethane", "urgency", "urgent", "urging", "urologist", "urology", "usable", "usage", "useable", "used", "uselessly", "user", "usher", "usual", "utensil", "utility", "utilize", "utmost", "utopia", "utter", "vacancy", "vacant", "vacate", "vacation", "vagabond", "vagrancy", "vagrantly", "vaguely", "vagueness", "valiant", "valid", "valium", "valley", "valuables", "value", "vanilla", "vanish", "vanity", "vanquish", "vantage", "vaporizer", "variable", "variably", "varied", "variety", "various", "varmint", "varnish", "varsity", "varying", "vascular", "vaseline", "vastly", "vastness", "veal", "vegan", "veggie", "vehicular", "velcro", "velocity", "velvet", "vendetta", "vending", "vendor", "veneering", "vengeful", "venomous", "ventricle", "venture", "venue", "venus", "verbalize", "verbally", "verbose", "verdict", "verify", "verse", "version", "versus", "vertebrae", "vertical", "vertigo", "very", "vessel", "vest", "veteran", "veto", "vexingly", "viability", "viable", "vibes", "vice", "vicinity", "victory", "video", "viewable", "viewer", "viewing", "viewless", "viewpoint", "vigorous", "village", "villain", "vindicate", "vineyard", "vintage", "violate", "violation", "violator", "violet", "violin", "viper", "viral", "virtual", "virtuous", "virus", "visa", "viscosity", "viscous", "viselike", "visible", "visibly", "vision", "visiting", "visitor", "visor", "vista", "vitality", "vitalize", "vitally", "vitamins", "vivacious", "vividly", "vividness", "vixen", "vocalist", "vocalize", "vocally", "vocation", "voice", "voicing", "void", "volatile", "volley", "voltage", "volumes", "voter", "voting", "voucher", "vowed", "vowel", "voyage", "wackiness", "wad", "wafer", "waffle", "waged", "wager", "wages", "waggle", "wagon", "wake", "waking", "walk", "walmart", "walnut", "walrus", "waltz", "wand", "wannabe", "wanted", "wanting", "wasabi", "washable", "washbasin", "washboard", "washbowl", "washcloth", "washday", "washed", "washer", "washhouse", "washing", "washout", "washroom", "washstand", "washtub", "wasp", "wasting", "watch", "water", "waviness", "waving", "wavy", "whacking", "whacky", "wham", "wharf", "wheat", "whenever", "whiff", "whimsical", "whinny", "whiny", "whisking", "whoever", "whole", "whomever", "whoopee", "whooping", "whoops", "why", "wick", "widely", "widen", "widget", "widow", "width", "wieldable", "wielder", "wife", "wifi", "wikipedia", "wildcard", "wildcat", "wilder", "wildfire", "wildfowl", "wildland", "wildlife", "wildly", "wildness", "willed", "willfully", "willing", "willow", "willpower", "wilt", "wimp", "wince", "wincing", "wind", "wing", "winking", "winner", "winnings", "winter", "wipe", "wired", "wireless", "wiring", "wiry", "wisdom", "wise", "wish", "wisplike", "wispy", "wistful", "wizard", "wobble", "wobbling", "wobbly", "wok", "wolf", "wolverine", "womanhood", "womankind", "womanless", "womanlike", "womanly", "womb", "woof", "wooing", "wool", "woozy", "word", "work", "worried", "worrier", "worrisome", "worry", "worsening", "worshiper", "worst", "wound", "woven", "wow", "wrangle", "wrath", "wreath", "wreckage", "wrecker", "wrecking", "wrench", "wriggle", "wriggly", "wrinkle", "wrinkly", "wrist", "writing", "written", "wrongdoer", "wronged", "wrongful", "wrongly", "wrongness", "wrought", "xbox", "xerox", "yahoo", "yam", "yanking", "yapping", "yard", "yarn", "yeah", "yearbook", "yearling", "yearly", "yearning", "yeast", "yelling", "yelp", "yen", "yesterday", "yiddish", "yield", "yin", "yippee", "yo-yo", "yodel", "yoga", "yogurt", "yonder", "yoyo", "yummy", "zap", "zealous", "zebra", "zen", "zeppelin", "zero", "zestfully", "zesty", "zigzagged", "zipfile", "zipping", "zippy", "zips", "zit", "zodiac", "zombie", "zone", "zoning", "zookeeper", "zoologist", "zoology", "zoom", ];