diff options
35 files changed, 3848 insertions, 1453 deletions
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..c02a77b --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,67 @@ +name: tests +on: + push: + branches: [main] + pull_request: {} +env: + RUST_BACKTRACE: 1 +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - run: cargo build --all-targets + build-musl: + runs-on: ubuntu-latest + steps: + - run: sudo apt-get install clang-11 + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl + - run: TARGET_CC=clang-11 TARGET_AR=llvm-ar-11 cargo build --all-targets --target x86_64-unknown-linux-musl + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - run: cargo build --all-targets + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test + test-musl: + runs-on: ubuntu-latest + steps: + - run: sudo apt-get install clang-11 + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl + - run: TARGET_CC=clang-11 TARGET_AR=llvm-ar-11 cargo test --target x86_64-unknown-linux-musl + test-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - run: cargo install --locked --debug cargo-deny + - run: cargo clippy --all-targets -- -Dwarnings + - run: cargo fmt --check + - run: cargo deny check + doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - run: cargo doc diff --git a/.rustfmt.toml b/.rustfmt.toml index 55b0b14..7b6182f 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,2 +1,2 @@ -edition = "2018" +edition = "2021" max_width = 78 diff --git a/CHANGELOG.md b/CHANGELOG.md index 23143ec..4a3dfde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,119 @@ # Changelog +## [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 @@ -3,52 +3,117 @@ version = 3 [[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] name = "aes" -version = "0.7.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", "cpufeatures", - "opaque-debug", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] -name = "ansi_term" -version = "0.11.0" +name = "anstream" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", ] [[package]] name = "anyhow" -version = "1.0.44" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca87830a3e3fb156dc96cfbd31cb620265dd053be734723f22b760d6cc3c3051" + +[[package]] +name = "argon2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] [[package]] name = "arrayvec" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-trait" -version = "0.1.51" +version = "0.1.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" dependencies = [ "proc-macro2", "quote", @@ -56,27 +121,25 @@ dependencies = [ ] [[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] name = "autocfg" -version = "0.1.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] -name = "autocfg" -version = "1.0.1" +name = "backtrace" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] [[package]] name = "base32" @@ -86,15 +149,15 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base64" -version = "0.13.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" -version = "1.0.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" @@ -103,59 +166,105 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] -name = "block-modes" -version = "0.8.1" +name = "block-padding" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "block-padding", - "cipher", + "generic-array", ] [[package]] -name = "block-padding" -version = "0.2.1" +name = "bumpalo" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] -name = "boxfnonce" -version = "0.1.1" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "bumpalo" -version = "3.8.0" +name = "bytes" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] -name = "byteorder" -version = "1.4.3" +name = "calloop" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "7b50b5a44d59a98c55a9eeb518f39bf7499ba19fd98ee7d22618687f3f10adbf" +dependencies = [ + "bitflags 2.4.1", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] [[package]] -name = "bytes" -version = "1.1.0" +name = "calloop-wayland-source" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[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.0.71" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -165,40 +274,114 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cipher" -version = "0.3.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "generic-array", + "crypto-common", + "inout", ] [[package]] name = "clap" -version = "2.33.3" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" dependencies = [ - "ansi_term", - "atty", - "bitflags", + "anstream", + "anstyle", + "clap_lex", "strsim", - "term_size", - "textwrap", - "unicode-width", - "vec_map", + "terminal_size", +] + +[[package]] +name = "clap_complete" +version = "4.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a51919c5608a32e34ea1d6be321ad070065e17613e168c5b6977024290f2630b" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", ] [[package]] name = "const-oid" -version = "0.6.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "copypasta" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d35364349bf9e9e1c3a035ddcb00d188d23a3c40c50244c03c27a99fc6a65ae" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -206,112 +389,162 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] [[package]] -name = "crypto-bigint" -version = "0.2.11" +name = "crossbeam-utils" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" dependencies = [ - "generic-array", - "rand_core", - "subtle", + "cfg-if", ] [[package]] -name = "crypto-mac" -version = "0.11.1" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "subtle", + "typenum", ] [[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] name = "daemonize" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c24513e34f53b640819f0ac9f705b673fcf4006d7aab8778bee72ebfc89815" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" dependencies = [ - "boxfnonce", "libc", ] [[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] name = "der" -version = "0.4.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e98c534e9c8a0483aa01d6f6913bc063de254311bd267c9cf535e9b70e15b2" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", - "crypto-bigint", + "pem-rfc7468", + "zeroize", ] [[package]] name = "digest" -version = "0.9.0" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "generic-array", + "block-buffer", + "const-oid", + "crypto-common", + "subtle", ] [[package]] name = "directories" -version = "4.0.1" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +checksum = "74be3be809c18e089de43bdc504652bb2bc473fca8756131f8689db8cf079ba9" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.6" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" dependencies = [ "libc", "redox_users", - "winapi", + "windows-sys 0.45.0", ] [[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] name = "encoding_rs" -version = "0.8.29" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] [[package]] name = "env_logger" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ - "atty", "humantime", + "is-terminal", "log", "regex", "termcolor", ] [[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -319,43 +552,67 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "matches", "percent-encoding", ] [[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "autocfg 1.0.1", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -363,50 +620,59 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ - "autocfg 1.0.1", + "futures-channel", "futures-core", "futures-io", "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +dependencies = [ + "libc", + "winapi", +] + +[[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -414,10 +680,16 @@ dependencies = [ ] [[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] name = "h2" -version = "0.3.7" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -434,53 +706,45 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hkdf" -version = "0.11.0" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "digest", "hmac", ] [[package]] name = "hmac" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "crypto-mac", "digest", ] [[package]] name = "http" -version = "0.2.5" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -489,9 +753,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -500,15 +764,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.5.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -518,9 +782,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.14" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -542,108 +806,149 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", + "http", "hyper", - "log", "rustls", "tokio", "tokio-rustls", - "webpki", ] [[package]] name = "idna" -version = "0.2.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" -version = "1.7.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ - "autocfg 1.0.1", + "equivalent", "hashbrown", ] [[package]] -name = "instant" -version = "0.1.12" +name = "inout" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "cfg-if", + "block-padding", + "generic-array", ] [[package]] name = "ipnet" -version = "2.3.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] [[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" dependencies = [ - "spin", + "spin 0.5.2", ] [[package]] name = "libc" -version = "0.2.105" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "libloading" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] [[package]] name = "libm" -version = "0.2.1" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "mach" @@ -655,83 +960,83 @@ dependencies = [ ] [[package]] -name = "matches" -version = "0.1.9" +name = "malloc_buf" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] [[package]] name = "memchr" -version = "2.4.1" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memmap2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45fd3a57831bf88bc63f8cebc0cf956116276e97fef3966103e96416209f7c92" +dependencies = [ + "libc", +] [[package]] name = "memoffset" -version = "0.6.4" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ - "autocfg 1.0.1", + "autocfg", ] [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "mio" -version = "0.7.14" +name = "miniz_oxide" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", + "adler", ] [[package]] -name = "miow" -version = "0.3.7" +name = "mio" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ - "winapi", + "libc", + "wasi", + "windows-sys 0.48.0", ] [[package]] name = "nix" -version = "0.23.0" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ - "bitflags", - "cc", + "bitflags 1.3.2", "cfg-if", "libc", "memoffset", -] - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", + "pin-utils", ] [[package]] name = "num-bigint-dig" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ - "autocfg 0.1.7", "byteorder", "lazy_static", "libm", @@ -745,93 +1050,123 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg 1.0.1", + "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ - "autocfg 1.0.1", + "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ - "autocfg 1.0.1", + "autocfg", "libm", ] [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] -name = "once_cell" -version = "1.8.0" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] [[package]] -name = "opaque-debug" -version = "0.3.0" +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl-probe" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", - "instant", "libc", "redox_syscall", "smallvec", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "password-hash" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core", @@ -839,64 +1174,41 @@ dependencies = [ ] [[package]] -name = "paw" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c0fc9b564dbc3dc2ed7c92c0c144f4de340aa94514ce2b446065417c4084e9" -dependencies = [ - "paw-attributes", - "paw-raw", -] - -[[package]] -name = "paw-attributes" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f35583365be5d148e959284f42526841917b7bfa09e2d1a7ad5dde2cf0eaa39" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "paw-raw" -version = "1.0.0" +name = "paste" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f0b59668fe80c5afe998f0c0bf93322bf2cd66cafeeb80581f291716f3467f2" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pbkdf2" -version = "0.9.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "crypto-mac", + "digest", "hmac", - "password-hash", - "sha2", ] [[package]] name = "pem-rfc7468" -version = "0.2.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f22eb0e3c593294a99e9ff4b24cf6b752d43f193aa4415fe5077c159996d497" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -906,98 +1218,87 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" -version = "0.2.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "116bee8279d783c0cf370efa1a94632f2108e5ef0bb32df31f051647810a4e2c" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der", - "pem-rfc7468", - "zeroize", + "pkcs8", + "spki", ] [[package]] name = "pkcs8" -version = "0.7.6" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", - "pem-rfc7468", - "pkcs1", "spki", - "zeroize", ] [[package]] -name = "ppv-lite86" -version = "0.2.15" +name = "pkg-config" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "polling" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", ] [[package]] -name = "proc-macro-hack" -version = "0.5.19" +name = "ppv-lite86" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "proc-macro-nested" -version = "0.1.7" +name = "proc-macro2" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" +checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1" +dependencies = [ + "unicode-ident", +] [[package]] -name = "proc-macro2" -version = "1.0.32" +name = "quick-xml" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ - "unicode-xid", + "memchr", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "22a37c9326af5ed140c86a46655b5278de879853be5573c01df185b6f49a580a" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", - "rand_hc", ] [[package]] @@ -1012,62 +1313,63 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - -[[package]] name = "rbw" -version = "1.4.1" +version = "1.9.0" dependencies = [ "aes", "anyhow", + "argon2", "arrayvec", "async-trait", "base32", "base64", - "block-modes", "block-padding", + "cbc", + "clap", + "clap_complete", + "copypasta", "daemonize", "directories", "env_logger", + "futures", + "futures-channel", + "futures-util", "hkdf", "hmac", "humantime", + "is-terminal", "libc", "log", "nix", - "paw", "pbkdf2", "percent-encoding", + "pkcs8", "rand", "region", "reqwest", + "rmpv", "rsa", "serde", "serde_json", "serde_path_to_error", "serde_repr", - "sha-1", + "sha1", "sha2", - "structopt", "tempfile", - "term_size", + "terminal_size", "textwrap", "thiserror", "tokio", + "tokio-stream", + "tokio-tungstenite", "totp-lite", "url", "uuid", @@ -1076,28 +1378,41 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", - "redox_syscall", + "libredox", + "thiserror", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", @@ -1106,9 +1421,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "region" @@ -1116,50 +1431,45 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e189c2369884dce920945e2ddf79b3dff49e071a167dd1817fa9c4c00d512e" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "mach", "winapi", ] [[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] name = "reqwest" -version = "0.11.6" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "hyper", "hyper-rustls", "ipnet", "js-sys", - "lazy_static", "log", "mime", + "once_cell", "percent-encoding", "pin-project-lite", "rustls", "rustls-native-certs", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-rustls", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1169,91 +1479,153 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", + "getrandom", "libc", - "once_cell", - "spin", + "spin 0.9.8", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.48.0", ] [[package]] -name = "rsa" -version = "0.5.0" +name = "rmp" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" +checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" dependencies = [ "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmpv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0e0214a4a2b444ecce41a4025792fc31f77c7bb89c46d253953ea8c65701ec" +dependencies = [ + "num-traits", + "rmp", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", "digest", - "lazy_static", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", - "rand", + "rand_core", + "signature", + "spki", "subtle", "zeroize", ] [[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] name = "rustls" -version = "0.19.1" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ - "base64", "log", "ring", + "rustls-webpki", "sct", - "webpki", ] [[package]] name = "rustls-native-certs" -version = "0.5.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls", + "rustls-pemfile", "schannel", "security-framework", ] [[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] name = "ryu" -version = "1.0.5" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" -version = "0.1.19" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "lazy_static", - "winapi", + "windows-sys 0.52.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.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", "untrusted", @@ -1261,11 +1633,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.4.2" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -1274,9 +1646,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.4.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -1284,18 +1656,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", @@ -1304,9 +1676,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" dependencies = [ "itoa", "ryu", @@ -1315,18 +1687,19 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.5" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0421d4f173fab82d72d6babf36d57fae38b994ca5c2d78e704260ba6d12118b" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ + "itoa", "serde", ] [[package]] name = "serde_repr" -version = "0.1.7" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", @@ -1335,9 +1708,9 @@ dependencies = [ [[package]] name = "serde_urlencoded" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", @@ -1346,60 +1719,111 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.9.8" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "block-buffer", "cfg-if", "cpufeatures", "digest", - "opaque-debug", ] [[package]] name = "sha2" -version = "0.9.8" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "block-buffer", "cfg-if", "cpufeatures", "digest", - "opaque-debug", ] [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 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.5" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.7.0" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "smithay-client-toolkit" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e3d9941fa3bacf7c2bf4b065304faa14164151254cd16ce1b1bc8fc381600f" +dependencies = [ + "bitflags 2.4.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "0bb62b280ce5a5cba847669933a0948d00904cf83845c944eae96a4738cea1a6" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] [[package]] name = "socket2" -version = "0.4.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1409,131 +1833,122 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] name = "spki" -version = "0.4.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ + "base64ct", "der", ] [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] -name = "structopt" -version = "0.3.25" +name = "subtle" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" -dependencies = [ - "clap", - "lazy_static", - "paw", - "structopt-derive", -] +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] -name = "structopt-derive" -version = "0.4.18" +name = "syn" +version = "2.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +checksum = "0eae3c679c56dc214320b67a1bc04ef3dfbd6411f6443974b5e4893231298e66" dependencies = [ - "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn", + "unicode-ident", ] [[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.81" +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "system-configuration-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", + "core-foundation-sys", + "libc", ] [[package]] name = "tempfile" -version = "3.2.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", - "libc", - "rand", + "fastrand", "redox_syscall", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys 0.52.0", ] [[package]] -name = "term_size" -version = "0.3.2" +name = "termcolor" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ - "libc", - "winapi", + "winapi-util", ] [[package]] -name = "termcolor" -version = "1.1.2" +name = "terminal_size" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "winapi-util", + "rustix", + "windows-sys 0.48.0", ] [[package]] name = "textwrap" -version = "0.11.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" dependencies = [ - "term_size", + "smawk", + "unicode-linebreak", "unicode-width", ] [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "b2cd5904763bad08ad5513ddbb12cf2ae273ca53fa9f68e843e236ec6dfccc09" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "3dcf4a824cce0aeacd6f38ae6f24234c8e80d68632338ebaa1443b5df9e29e19" dependencies = [ "proc-macro2", "quote", @@ -1542,44 +1957,43 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.12.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ - "autocfg 1.0.1", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.5.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -1588,172 +2002,220 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", "tokio", - "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tungstenite", ] [[package]] name = "tokio-util" -version = "0.6.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", "pin-project-lite", "tokio", + "tracing", ] [[package]] name = "totp-lite" -version = "1.0.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18009e8be74bfb2e2cc59a63d078d95c042858a1ca1128a294e1f9ce225148b" +checksum = "f8e43134db17199f7f721803383ac5854edd0d3d523cc34dba321d6acfbe76c3" dependencies = [ "digest", "hmac", - "sha-1", + "sha1", "sha2", ] [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "rustls", + "sha1", + "thiserror", + "url", + "utf-8", +] [[package]] name = "typenum" -version = "1.14.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] -name = "unicode-normalization" -version = "0.1.19" +name = "unicode-ident" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" -dependencies = [ - "tinyvec", -] +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "unicode-segmentation" -version = "1.8.0" +name = "unicode-linebreak" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] -name = "unicode-width" -version = "0.1.9" +name = "unicode-normalization" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-width" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.2.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] [[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] name = "uuid" -version = "0.8.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", ] [[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1761,13 +2223,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -1776,9 +2238,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.28" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -1788,9 +2250,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1798,9 +2260,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", @@ -1811,28 +2273,114 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] -name = "web-sys" -version = "0.3.55" +name = "wayland-backend" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4" dependencies = [ - "js-sys", - "wasm-bindgen", + "cc", + "downcast-rs", + "nix", + "scoped-tls", + "smallvec", + "wayland-sys", ] [[package]] -name = "webpki" -version = "0.21.4" +name = "wayland-client" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" dependencies = [ - "ring", - "untrusted", + "bitflags 2.4.1", + "nix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.4.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44aa20ae986659d6c77d64d808a046996a932aa763913864dc40c359ef7ad5b" +dependencies = [ + "nix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" +dependencies = [ + "bitflags 2.4.1", + "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.4.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", ] [[package]] @@ -1853,9 +2401,18 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" dependencies = [ "winapi", ] @@ -1867,31 +2424,258 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] name = "winreg" -version = "0.7.0" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] -name = "zeroize" -version = "1.4.2" +name = "x11-clipboard" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" +checksum = "b41aca1115b1f195f21c541c5efb423470848d48143127d0f07f8b90c27440df" dependencies = [ - "zeroize_derive", + "x11rb", ] [[package]] -name = "zeroize_derive" -version = "1.2.0" +name = "x11rb" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdff2024a851a322b08f179173ae2ba620445aef1e838f0c196820eade4ae0c7" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", + "gethostname", + "nix", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", ] + +[[package]] +name = "x11rb-protocol" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" +dependencies = [ + "nix", +] + +[[package]] +name = "xcursor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" + +[[package]] +name = "xkeysym" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" @@ -1,8 +1,8 @@ [package] name = "rbw" -version = "1.4.1" +version = "1.9.0" authors = ["Jesse Luehrs <doy@tozt.net>"] -edition = "2018" +edition = "2021" description = "Unofficial Bitwarden CLI" repository = "https://git.tozt.net/rbw" @@ -10,48 +10,61 @@ readme = "README.md" keywords = ["bitwarden"] categories = ["command-line-utilities", "cryptography"] license = "MIT" +include = ["src/**/*", "bin/**/*", "LICENSE", "README.md", "CHANGELOG.md"] [dependencies] -aes = "0.7" -anyhow = "1.0" -arrayvec = "0.7" -async-trait = "0.1" -base32 = "0.4" -base64 = "0.13" -block-modes = "0.8" -block-padding = "0.2" -daemonize = "0.4" -directories = "4.0" -env_logger = "0.9" -hkdf = "0.11" -hmac = { version = "0.11", features = ["std"] } -humantime = "2.1" -libc = "0.2" -log = "0.4" -nix = "0.23" -paw = "1.0" -pbkdf2 = "0.9" -percent-encoding = "2.1" -rand = "0.8" -region = "3.0" -reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls-native-roots"] } -rsa = "0.5" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_path_to_error = "0.1" -serde_repr = "0.1" -sha-1 = "0.9" -sha2 = "0.9" -structopt = { version = "0.3", features = ["paw", "wrap_help"] } -tempfile = "3.2" -term_size = "0.3" -textwrap = "0.11" -thiserror = "1.0" -tokio = { version = "1.12", features = ["full"] } -totp-lite = "1.0" -url = "2.2" -uuid = { version = "0.8", features = ["v4"] } -zeroize = "1.4" +aes = "0.8.3" +anyhow = "1.0.78" +argon2 = "0.5.2" +arrayvec = "0.7.4" +async-trait = "0.1.76" +base32 = "0.4.0" +base64 = "0.21.5" +block-padding = "0.3.3" +cbc = { version = "0.1.2", features = ["alloc", "std"] } +clap = { version = "4.4.12", features = ["wrap_help", "derive"] } +clap_complete = "4.4.5" +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.10.1" +futures = "0.3.30" +futures-channel = "0.3.30" +futures-util = "0.3.30" +hkdf = "0.12.4" +hmac = { version = "0.12.1", features = ["std"] } +humantime = "2.1.0" +libc = "0.2.151" +log = "0.4.20" +nix = "0.26" +pbkdf2 = "0.12.2" +percent-encoding = "2.3.1" +pkcs8 = "0.10.2" +rand = "0.8.5" +region = "3.0.0" +reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "rustls-tls-native-roots"] } +rsa = "0.9.6" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.109" +serde_path_to_error = "0.1.14" +serde_repr = "0.1.17" +sha1 = "0.10.6" +sha2 = "0.10.8" +tempfile = "3.9.0" +terminal_size = "0.3.0" +textwrap = "0.16.0" +thiserror = "1.0.53" +tokio = { version = "1.35.1", features = ["full"] } +tokio-stream = { version = "0.1.14", features = ["net"] } +totp-lite = "2.0.1" +url = "2.5.0" +uuid = { version = "1.6.1", features = ["v4"] } +zeroize = "1.7.0" +copypasta = "0.10.0" +rmpv = "1.0.1" +tokio-tungstenite = { version = "0.20", features = ["rustls-tls-native-roots"] } +is-terminal = "0.4.10" [package.metadata.deb] depends = "pinentry" @@ -1,7 +1,8 @@ -NAME = $(shell cargo metadata --no-deps --format-version 1 | jq '.packages[0].name') -VERSION = $(shell cargo metadata --no-deps --format-version 1 | jq '.packages[0].version') +NAME = $(shell cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].name') +VERSION = $(shell cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') DEB_PACKAGE = $(NAME)_$(VERSION)_amd64.deb +TGZ_PACKAGE = $(NAME)_$(VERSION)_linux_amd64.tar.gz all: build .PHONY: all @@ -41,7 +42,7 @@ completion: release @./target/x86_64-unknown-linux-musl/release/rbw gen-completions fish > target/x86_64-unknown-linux-musl/release/completion/fish .PHONY: completion -package: pkg/$(DEB_PACKAGE) +package: pkg/$(DEB_PACKAGE) pkg/$(TGZ_PACKAGE) .PHONY: package pkg: @@ -51,13 +52,16 @@ pkg/$(DEB_PACKAGE): release completion | pkg @cargo deb --no-build --target x86_64-unknown-linux-musl && mv target/x86_64-unknown-linux-musl/debian/$(DEB_PACKAGE) pkg pkg/$(DEB_PACKAGE).minisig: pkg/$(DEB_PACKAGE) - @minisign -Sm pkg/$(DEB_PACKAGE) + @minisign -Sm $< + +pkg/$(TGZ_PACKAGE): release completion | pkg + @tar czf $@ -C target/x86_64-unknown-linux-musl/release rbw rbw-agent completion release-dir-deb: @ssh tozt.net mkdir -p releases/rbw/deb .PHONY: release-dir-deb -publish: publish-crates-io publish-git-tags publish-deb +publish: publish-crates-io publish-git-tags publish-deb publish-github .PHONY: publish publish-crates-io: test @@ -74,3 +78,6 @@ publish-git-tags: test publish-deb: test pkg/$(DEB_PACKAGE) pkg/$(DEB_PACKAGE).minisig release-dir-deb @scp pkg/$(DEB_PACKAGE) pkg/$(DEB_PACKAGE).minisig tozt.net:releases/rbw/deb .PHONY: publish-deb + +publish-github: test pkg/$(TGZ_PACKAGE) + @perl -nle'print if /^## \Q[$(VERSION)]/../^## (?!\Q[$(VERSION)]\E)/' CHANGELOG.md | head -n-2 | gh release create $(VERSION) --verify-tag --notes-file - pkg/$(TGZ_PACKAGE) @@ -23,8 +23,8 @@ and merge pull requests implementing those features. ### Arch Linux -`rbw` is available in the [community -repository](https://archlinux.org/packages/community/x86_64/rbw/). +`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. @@ -37,6 +37,11 @@ You can download a Debian package from [`minisign`](https://github.com/jedisct1/minisign), and can be verified using the public key `RWTM0AZ5RpROOfAIWx1HvYQ6pw1+FKwN6526UFTKNImP/Hz3ynCFst3r`. +### 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 @@ -56,9 +61,15 @@ configuration options: * `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. +* `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`. @@ -82,7 +93,9 @@ out by running `rbw purge`, and you can explicitly lock the database by running functionality. Run `rbw get <name>` to get your passwords. If you also want to get the username -or the note associated, you can use the flag `--full`. +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. *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 @@ -95,3 +108,5 @@ 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 diff --git a/bin/rbw-fzf b/bin/rbw-fzf index cbf15c4..308dd89 100755 --- a/bin/rbw-fzf +++ b/bin/rbw-fzf @@ -2,4 +2,9 @@ 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 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 diff --git a/bin/rbw-pinentry-keyring b/bin/rbw-pinentry-keyring new file mode 100755 index 0000000..1626853 --- /dev/null +++ b/bin/rbw-pinentry-keyring @@ -0,0 +1,72 @@ +#!/bin/bash + +[[ -z "${RBW_PROFILE}" ]] && rbw_profile='rbw' || rbw_profile="rbw-${RBW_PROFILE}" + +set -eEuo pipefail + +function help() { + cat <<EOHELP +Use this script as pinentry to store master password for rbw into your keyring + +Usage +- run "rbw-pinentry-keyring setup" once to save master password to keyring +- add "rbw-pinentry-keyring" as "pinentry" in rbw config (${XDG_CONFIG_HOME}/rbw/config.json) +- use rbw as normal +Notes +- needs "secret-tool" to access keyring +- setup tested with pinentry-gnome3, but you can run the "secret-tool store"-command manually as well +- master passwords are stored into the keyring as plaintext, so secure your keyring appropriately +- supports multiple profiles, simply set RBW_PROFILE during setup +- can easily be rewritten to use other backends than keyring by setting the "secret_value"-variable +EOHELP +} + +function setup() { + cmd="SETTITLE rbw\n" + cmd+="SETPROMPT Master Password\n" + cmd+="SETDESC Please enter the master password for '$rbw_profile'\n" + cmd+="GETPIN\n" + password="$(printf "$cmd" | pinentry | grep -E "^D " | cut -d' ' -f2)" + if [ -n "$password" ]; then + echo -n "$password" | secret-tool store --label="$rbw_profile master password" application rbw profile "$rbw_profile" type master_password + fi +} + +function getpin() { + echo 'OK' + + while IFS=' ' read -r command args ; do + case "$command" in + SETPROMPT|SETTITLE| SETDESC) + echo 'OK' + ;; + GETPIN) + secret_value="$(secret-tool lookup application rbw profile "$rbw_profile" type master_password)" + if [ -z "$secret_value" ]; then + exit 1 + fi + printf 'D %s\n' "$secret_value" + echo 'OK' + ;; + BYE) + exit + ;; + *) + echo 'ERR Unknown command' + ;; + esac + done +} + +command="$1" +case "$command" in + -h|--help|help) + help + ;; + -s|--setup|setup) + setup + ;; + *) + getpin + ;; +esac @@ -1,24 +1,50 @@ targets = [ { triple = "x86_64-unknown-linux-musl" }, { triple = "x86_64-unknown-linux-gnu" }, + { triple = "x86_64-apple-darwin" }, + { triple = "aarch64-apple-darwin" }, ] [advisories] yanked = "deny" unsound = "deny" +ignore = [ + # this is only an unmaintained warning, and will hopefully be addressed + # by https://github.com/darfink/region-rs/pull/27 + "RUSTSEC-2020-0168", + # this is a timing attack against using the rsa crate for encryption, but + # we only use rsa decryption here + "RUSTSEC-2023-0071", +] [bans] +multiple-versions = "deny" +wildcards = "deny" deny = [ { name = "openssl-sys" }, ] skip = [ - # this is pulled in by rsa -> num-bigint-dig, but it's just a build dep so - # i don't care much - { name = "autocfg", version = "0.1.7" } + # the ecosystem is pretty split on these at the moment, should keep an + # eye on this to remove once more things have standardized on version 2 + { name = "bitflags", version = "1.3.2" }, + { name = "bitflags", version = "2.4.1" }, + + # see https://github.com/dignifiedquire/num-bigint/pull/58 and + # https://github.com/RustCrypto/RSA/issues/390 which should hopefully + # resolve this soon + { name = "spin", version = "0.5.2" }, + { name = "spin", version = "0.9.8" }, ] [licenses] -allow = ["MIT", "BSD-3-Clause", "Apache-2.0", "ISC"] +allow = [ + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "Apache-2.0", + "ISC", + "Unicode-DFS-2016", +] copyleft = "deny" exceptions = [ { name = "ring", allow = ["OpenSSL", "MIT", "ISC"] } @@ -31,3 +57,11 @@ expression = "MIT AND ISC AND OpenSSL" license-files = [ { path = "LICENSE", hash = 0xbd0eed23 } ] + +[[licenses.clarify]] +name = "encoding_rs" +version = "*" +expression = "(Apache-2.0 OR MIT) AND BSD-3-Clause" +license-files = [ + { path = "COPYRIGHT", hash = 0x39f8ad31 } +] diff --git a/src/actions.rs b/src/actions.rs index 02ec854..b07cf44 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -4,11 +4,11 @@ pub async fn register( email: &str, apikey: crate::locked::ApiKey, ) -> Result<()> { - let config = crate::config::Config::load_async().await?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, config) = api_client_async().await?; - client.register(email, &config.device_id, &apikey).await?; + client + .register(email, &crate::config::device_id(&config).await?, &apikey) + .await?; Ok(()) } @@ -18,40 +18,70 @@ pub async fn login( password: crate::locked::Password, two_factor_token: Option<&str>, two_factor_provider: Option<crate::api::TwoFactorProviderType>, -) -> Result<(String, String, u32, String)> { - let config = crate::config::Config::load_async().await?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); +) -> Result<( + String, + String, + crate::api::KdfType, + u32, + Option<u32>, + Option<u32>, + String, +)> { + let (client, config) = api_client_async().await?; + let (kdf, iterations, memory, parallelism) = + client.prelogin(email).await?; - let iterations = client.prelogin(email).await?; - let identity = - crate::identity::Identity::new(email, &password, iterations)?; + let identity = crate::identity::Identity::new( + email, + &password, + kdf, + iterations, + memory, + parallelism, + )?; let (access_token, refresh_token, protected_key) = client .login( email, - &config.device_id, + &crate::config::device_id(&config).await?, &identity.master_password_hash, two_factor_token, two_factor_provider, ) .await?; - Ok((access_token, refresh_token, iterations, protected_key)) + Ok(( + access_token, + refresh_token, + kdf, + iterations, + memory, + parallelism, + protected_key, + )) } -pub async fn unlock( +pub fn unlock<S: std::hash::BuildHasher>( email: &str, password: &crate::locked::Password, + kdf: crate::api::KdfType, iterations: u32, + memory: Option<u32>, + parallelism: Option<u32>, protected_key: &str, protected_private_key: &str, - protected_org_keys: &std::collections::HashMap<String, String>, + protected_org_keys: &std::collections::HashMap<String, String, S>, ) -> Result<( crate::locked::Keys, std::collections::HashMap<String, crate::locked::Keys>, )> { - let identity = - crate::identity::Identity::new(email, password, iterations)?; + let identity = crate::identity::Identity::new( + email, + password, + kdf, + iterations, + memory, + parallelism, + )?; let protected_key = crate::cipherstring::CipherString::new(protected_key)?; @@ -119,9 +149,7 @@ async fn sync_once( std::collections::HashMap<String, String>, Vec<crate::db::Entry>, )> { - let config = crate::config::Config::load_async().await?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client_async().await?; client.sync(access_token).await } @@ -145,10 +173,8 @@ fn add_once( notes: Option<&str>, folder_id: Option<&str>, ) -> Result<()> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); - client.add(access_token, name, data, notes, folder_id.as_deref())?; + let (client, _) = api_client()?; + client.add(access_token, name, data, notes, folder_id)?; Ok(()) } @@ -187,9 +213,7 @@ fn edit_once( folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<()> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.edit( access_token, id, @@ -214,9 +238,7 @@ pub fn remove( } fn remove_once(access_token: &str, id: &str) -> Result<()> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.remove(access_token, id)?; Ok(()) } @@ -231,9 +253,7 @@ pub fn list_folders( } fn list_folders_once(access_token: &str) -> Result<Vec<(String, String)>> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.folders(access_token) } @@ -248,9 +268,7 @@ pub fn create_folder( } fn create_folder_once(access_token: &str, name: &str) -> Result<String> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.create_folder(access_token, name) } @@ -300,15 +318,32 @@ where } fn exchange_refresh_token(refresh_token: &str) -> Result<String> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.exchange_refresh_token(refresh_token) } async fn exchange_refresh_token_async(refresh_token: &str) -> Result<String> { - let config = crate::config::Config::load_async().await?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + 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.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.client_cert_path(), + ); + Ok((client, config)) +} @@ -1,9 +1,15 @@ +// serde_repr generates some as conversions that we can't seem to silence from +// here, unfortunately +#![allow(clippy::as_conversions)] + use crate::prelude::*; use crate::json::{ DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _, }; +use tokio::io::AsyncReadExt as _; + #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, @@ -35,7 +41,7 @@ impl std::fmt::Display for UriMatchType { RegularExpression => "regular_expression", Never => "never", }; - write!(f, "{}", s) + write!(f, "{s}") } } @@ -51,6 +57,33 @@ pub enum TwoFactorProviderType { WebAuthn = 7, } +impl TwoFactorProviderType { + #[must_use] + 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." + } + } + + #[must_use] + pub fn header(&self) -> &str { + match *self { + Self::Authenticator => "Authenticator App", + Self::Yubikey => "Yubikey", + Self::Email => "Email Code", + _ => "Two Factor Authentication", + } + } + + #[must_use] + pub fn grab(&self) -> bool { + !matches!(self, Self::Email) + } +} + impl<'de> serde::Deserialize<'de> for TwoFactorProviderType { fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> where @@ -107,7 +140,7 @@ impl std::convert::TryFrom<u64> for TwoFactorProviderType { 6 => Ok(Self::OrganizationDuo), 7 => Ok(Self::WebAuthn), _ => Err(Error::InvalidTwoFactorProvider { - ty: format!("{}", ty), + ty: format!("{ty}"), }), } } @@ -131,6 +164,96 @@ impl std::str::FromStr for TwoFactorProviderType { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum KdfType { + Pbkdf2 = 0, + Argon2id = 1, +} + +impl<'de> serde::Deserialize<'de> for KdfType { + fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct KdfTypeVisitor; + impl<'de> serde::de::Visitor<'de> for KdfTypeVisitor { + type Value = KdfType; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + formatter.write_str("kdf id") + } + + fn visit_str<E>( + self, + value: &str, + ) -> std::result::Result<Self::Value, E> + where + E: serde::de::Error, + { + value.parse().map_err(serde::de::Error::custom) + } + + fn visit_u64<E>( + self, + value: u64, + ) -> std::result::Result<Self::Value, E> + 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<u64> for KdfType { + type Error = Error; + + fn try_from(ty: u64) -> Result<Self> { + 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<Self> { + match ty { + "0" => Ok(Self::Pbkdf2), + "1" => Ok(Self::Argon2id), + _ => Err(Error::InvalidKdfType { ty: ty.to_string() }), + } + } +} + +impl serde::Serialize for KdfType { + fn serialize<S>( + &self, + serializer: S, + ) -> std::result::Result<S::Ok, S::Error> + 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, @@ -138,10 +261,14 @@ struct PreloginReq { #[derive(serde::Deserialize, Debug)] struct PreloginRes { - #[serde(rename = "Kdf")] - kdf: u32, - #[serde(rename = "KdfIterations")] + #[serde(rename = "Kdf", alias = "kdf")] + kdf: KdfType, + #[serde(rename = "KdfIterations", alias = "kdfIterations")] kdf_iterations: u32, + #[serde(rename = "KdfMemory", alias = "kdfMemory")] + kdf_memory: Option<u32>, + #[serde(rename = "KdfParallelism", alias = "kdfParallelism")] + kdf_parallelism: Option<u32>, } #[derive(serde::Serialize, Debug)] @@ -169,10 +296,8 @@ struct ConnectPasswordReq { #[derive(serde::Deserialize, Debug)] struct ConnectPasswordRes { access_token: String, - expires_in: u32, - token_type: String, refresh_token: String, - #[serde(rename = "Key")] + #[serde(rename = "Key", alias = "key")] key: String, } @@ -180,15 +305,15 @@ struct ConnectPasswordRes { struct ConnectErrorRes { error: String, error_description: Option<String>, - #[serde(rename = "ErrorModel")] + #[serde(rename = "ErrorModel", alias = "errorModel")] error_model: Option<ConnectErrorResErrorModel>, - #[serde(rename = "TwoFactorProviders")] + #[serde(rename = "TwoFactorProviders", alias = "twoFactorProviders")] two_factor_providers: Option<Vec<TwoFactorProviderType>>, } #[derive(serde::Deserialize, Debug)] struct ConnectErrorResErrorModel { - #[serde(rename = "Message")] + #[serde(rename = "Message", alias = "message")] message: String, } @@ -202,46 +327,43 @@ struct ConnectRefreshTokenReq { #[derive(serde::Deserialize, Debug)] struct ConnectRefreshTokenRes { access_token: String, - expires_in: u32, - token_type: String, - refresh_token: String, } #[derive(serde::Deserialize, Debug)] struct SyncRes { - #[serde(rename = "Ciphers")] + #[serde(rename = "Ciphers", alias = "ciphers")] ciphers: Vec<SyncResCipher>, - #[serde(rename = "Profile")] + #[serde(rename = "Profile", alias = "profile")] profile: SyncResProfile, - #[serde(rename = "Folders")] + #[serde(rename = "Folders", alias = "folders")] folders: Vec<SyncResFolder>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResCipher { - #[serde(rename = "Id")] + #[serde(rename = "Id", alias = "id")] id: String, - #[serde(rename = "FolderId")] + #[serde(rename = "FolderId", alias = "folderId")] folder_id: Option<String>, - #[serde(rename = "OrganizationId")] + #[serde(rename = "OrganizationId", alias = "organizationId")] organization_id: Option<String>, - #[serde(rename = "Name")] + #[serde(rename = "Name", alias = "name")] name: String, - #[serde(rename = "Login")] + #[serde(rename = "Login", alias = "login")] login: Option<CipherLogin>, - #[serde(rename = "Card")] + #[serde(rename = "Card", alias = "card")] card: Option<CipherCard>, - #[serde(rename = "Identity")] + #[serde(rename = "Identity", alias = "identity")] identity: Option<CipherIdentity>, - #[serde(rename = "SecureNote")] + #[serde(rename = "SecureNote", alias = "secureNote")] secure_note: Option<CipherSecureNote>, - #[serde(rename = "Notes")] + #[serde(rename = "Notes", alias = "notes")] notes: Option<String>, - #[serde(rename = "PasswordHistory")] + #[serde(rename = "PasswordHistory", alias = "passwordHistory")] password_history: Option<Vec<SyncResPasswordHistory>>, - #[serde(rename = "Fields")] + #[serde(rename = "Fields", alias = "fields")] fields: Option<Vec<SyncResField>>, - #[serde(rename = "DeletedDate")] + #[serde(rename = "DeletedDate", alias = "deletedDate")] deleted_date: Option<String>, } @@ -253,32 +375,37 @@ impl SyncResCipher { if self.deleted_date.is_some() { return None; } - let history = if let Some(history) = &self.password_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() - } else { - vec![] - }; + 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) = if let Some(folder_id) = &self.folder_id { - let mut folder_name = None; - for folder in folders { - if &folder.id == folder_id { - folder_name = Some(folder.name.clone()); + 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)) - } else { - (None, None) - }; + (folder_name, Some(folder_id)) + }); let data = if let Some(login) = &self.login { crate::db::EntryData::Login { username: login.username.clone(), @@ -332,7 +459,7 @@ impl SyncResCipher { } else { return None; }; - let fields = if let Some(fields) = &self.fields { + let fields = self.fields.as_ref().map_or_else(Vec::new, |fields| { fields .iter() .map(|field| crate::db::Field { @@ -340,9 +467,7 @@ impl SyncResCipher { value: field.value.clone(), }) .collect() - } else { - vec![] - }; + }); Some(crate::db::Entry { id: self.id.clone(), org_id: self.organization_id.clone(), @@ -359,101 +484,101 @@ impl SyncResCipher { #[derive(serde::Deserialize, Debug)] struct SyncResProfile { - #[serde(rename = "Key")] + #[serde(rename = "Key", alias = "key")] key: String, - #[serde(rename = "PrivateKey")] + #[serde(rename = "PrivateKey", alias = "privateKey")] private_key: String, - #[serde(rename = "Organizations")] + #[serde(rename = "Organizations", alias = "organizations")] organizations: Vec<SyncResProfileOrganization>, } #[derive(serde::Deserialize, Debug)] struct SyncResProfileOrganization { - #[serde(rename = "Id")] + #[serde(rename = "Id", alias = "id")] id: String, - #[serde(rename = "Key")] + #[serde(rename = "Key", alias = "key")] key: String, } #[derive(serde::Deserialize, Debug, Clone)] struct SyncResFolder { - #[serde(rename = "Id")] + #[serde(rename = "Id", alias = "id")] id: String, - #[serde(rename = "Name")] + #[serde(rename = "Name", alias = "name")] name: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherLogin { - #[serde(rename = "Username")] + #[serde(rename = "Username", alias = "username")] username: Option<String>, - #[serde(rename = "Password")] + #[serde(rename = "Password", alias = "password")] password: Option<String>, - #[serde(rename = "Totp")] + #[serde(rename = "Totp", alias = "totp")] totp: Option<String>, - #[serde(rename = "Uris")] + #[serde(rename = "Uris", alias = "uris")] uris: Option<Vec<CipherLoginUri>>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherLoginUri { - #[serde(rename = "Uri")] + #[serde(rename = "Uri", alias = "uri")] uri: Option<String>, - #[serde(rename = "Match")] + #[serde(rename = "Match", alias = "match")] match_type: Option<UriMatchType>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherCard { - #[serde(rename = "CardholderName")] + #[serde(rename = "CardholderName", alias = "cardHolderName")] cardholder_name: Option<String>, - #[serde(rename = "Number")] + #[serde(rename = "Number", alias = "number")] number: Option<String>, - #[serde(rename = "Brand")] + #[serde(rename = "Brand", alias = "brand")] brand: Option<String>, - #[serde(rename = "ExpMonth")] + #[serde(rename = "ExpMonth", alias = "expMonth")] exp_month: Option<String>, - #[serde(rename = "ExpYear")] + #[serde(rename = "ExpYear", alias = "expYear")] exp_year: Option<String>, - #[serde(rename = "Code")] + #[serde(rename = "Code", alias = "code")] code: Option<String>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherIdentity { - #[serde(rename = "Title")] + #[serde(rename = "Title", alias = "title")] title: Option<String>, - #[serde(rename = "FirstName")] + #[serde(rename = "FirstName", alias = "firstName")] first_name: Option<String>, - #[serde(rename = "MiddleName")] + #[serde(rename = "MiddleName", alias = "middleName")] middle_name: Option<String>, - #[serde(rename = "LastName")] + #[serde(rename = "LastName", alias = "lastName")] last_name: Option<String>, - #[serde(rename = "Address1")] + #[serde(rename = "Address1", alias = "address1")] address1: Option<String>, - #[serde(rename = "Address2")] + #[serde(rename = "Address2", alias = "address2")] address2: Option<String>, - #[serde(rename = "Address3")] + #[serde(rename = "Address3", alias = "address3")] address3: Option<String>, - #[serde(rename = "City")] + #[serde(rename = "City", alias = "city")] city: Option<String>, - #[serde(rename = "State")] + #[serde(rename = "State", alias = "state")] state: Option<String>, - #[serde(rename = "PostalCode")] + #[serde(rename = "PostalCode", alias = "postalCode")] postal_code: Option<String>, - #[serde(rename = "Country")] + #[serde(rename = "Country", alias = "country")] country: Option<String>, - #[serde(rename = "Phone")] + #[serde(rename = "Phone", alias = "phone")] phone: Option<String>, - #[serde(rename = "Email")] + #[serde(rename = "Email", alias = "email")] email: Option<String>, - #[serde(rename = "SSN")] + #[serde(rename = "SSN", alias = "ssn")] ssn: Option<String>, - #[serde(rename = "LicenseNumber")] + #[serde(rename = "LicenseNumber", alias = "licenseNumber")] license_number: Option<String>, - #[serde(rename = "PassportNumber")] + #[serde(rename = "PassportNumber", alias = "passportNumber")] passport_number: Option<String>, - #[serde(rename = "Username")] + #[serde(rename = "Username", alias = "username")] username: Option<String>, } @@ -464,19 +589,19 @@ struct CipherSecureNote {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResPasswordHistory { - #[serde(rename = "LastUsedDate")] + #[serde(rename = "LastUsedDate", alias = "lastUsedDate")] last_used_date: String, - #[serde(rename = "Password")] + #[serde(rename = "Password", alias = "password")] password: Option<String>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResField { - #[serde(rename = "Type")] + #[serde(rename = "Type", alias = "type")] ty: u32, - #[serde(rename = "Name")] + #[serde(rename = "Name", alias = "name")] name: Option<String>, - #[serde(rename = "Value")] + #[serde(rename = "Value", alias = "value")] value: Option<String>, } @@ -530,15 +655,15 @@ struct CiphersPutReqHistory { #[derive(serde::Deserialize, Debug)] struct FoldersRes { - #[serde(rename = "Data")] + #[serde(rename = "Data", alias = "data")] data: Vec<FoldersResData>, } #[derive(serde::Deserialize, Debug)] struct FoldersResData { - #[serde(rename = "Id")] + #[serde(rename = "Id", alias = "id")] id: String, - #[serde(rename = "Name")] + #[serde(rename = "Name", alias = "name")] name: String, } @@ -551,21 +676,64 @@ struct FoldersPostReq { pub struct Client { base_url: String, identity_url: String, + client_cert_path: Option<std::path::PathBuf>, } impl Client { - pub fn new(base_url: &str, identity_url: &str) -> Self { + #[must_use] + pub fn new( + base_url: &str, + identity_url: &str, + client_cert_path: Option<&std::path::Path>, + ) -> Self { Self { base_url: base_url.to_string(), identity_url: identity_url.to_string(), + client_cert_path: client_cert_path + .map(std::path::Path::to_path_buf), } } - pub async fn prelogin(&self, email: &str) -> Result<u32> { + async fn reqwest_client(&self) -> Result<reqwest::Client> { + 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::LoadClientCertReqwest { + source: e, + file: client_cert_path.clone(), + } + })?; + Ok(reqwest::Client::builder().identity(pem).build().map_err( + |e| Error::LoadClientCertReqwest { + source: e, + file: client_cert_path.clone(), + }, + )?) + } else { + Ok(reqwest::Client::new()) + } + } + + pub async fn prelogin( + &self, + email: &str, + ) -> Result<(KdfType, u32, Option<u32>, Option<u32>)> { let prelogin = PreloginReq { email: email.to_string(), }; - let client = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .post(&self.api_url("/accounts/prelogin")) .json(&prelogin) @@ -573,7 +741,12 @@ impl Client { .await .map_err(|source| Error::Reqwest { source })?; let prelogin_res: PreloginRes = res.json_with_path().await?; - Ok(prelogin_res.kdf_iterations) + Ok(( + prelogin_res.kdf, + prelogin_res.kdf_iterations, + prelogin_res.kdf_memory, + prelogin_res.kdf_parallelism, + )) } pub async fn register( @@ -596,22 +769,34 @@ impl Client { device_type: 8, device_identifier: device_id.to_string(), device_name: "rbw".to_string(), - device_push_token: "".to_string(), + device_push_token: String::new(), two_factor_token: None, two_factor_provider: None, }; - let client = reqwest::Client::new(); + 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 let reqwest::StatusCode::OK = res.status() { + if res.status() == reqwest::StatusCode::OK { Ok(()) } else { let code = res.status().as_u16(); - Err(classify_login_error(&res.json_with_path().await?, code)) + 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 }) + } + } } } @@ -626,30 +811,34 @@ impl Client { let connect_req = ConnectPasswordReq { grant_type: "password".to_string(), username: email.to_string(), - password: Some(base64::encode(password_hash.hash())), + password: Some(crate::base64::encode(password_hash.hash())), scope: "api offline_access".to_string(), client_id: "desktop".to_string(), client_secret: None, device_type: 8, device_identifier: device_id.to_string(), device_name: "rbw".to_string(), - device_push_token: "".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 = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) .header( "auth-email", - base64::encode_config(email, base64::URL_SAFE_NO_PAD), + crate::base64::encode_url_safe_no_pad(email), + ) + .header( + "user-agent", + format!("rbw/{}", env!("CARGO_PKG_VERSION")), ) .send() .await .map_err(|source| Error::Reqwest { source })?; - if let reqwest::StatusCode::OK = res.status() { + if res.status() == reqwest::StatusCode::OK { let connect_res: ConnectPasswordRes = res.json_with_path().await?; Ok(( @@ -659,7 +848,19 @@ impl Client { )) } else { let code = res.status().as_u16(); - Err(classify_login_error(&res.json_with_path().await?, code)) + 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 }) + } + } } } @@ -672,10 +873,10 @@ impl Client { std::collections::HashMap<String, String>, Vec<crate::db::Entry>, )> { - let client = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .get(&self.api_url("/sync")) - .header("Authorization", format!("Bearer {}", access_token)) + .header("Authorization", format!("Bearer {access_token}")) .send() .await .map_err(|source| Error::Reqwest { source })?; @@ -816,8 +1017,8 @@ impl Client { } let client = reqwest::blocking::Client::new(); let res = client - .post(&self.api_url("/ciphers")) - .header("Authorization", format!("Bearer {}", access_token)) + .post(self.api_url("/ciphers")) + .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; @@ -844,7 +1045,12 @@ impl Client { history: &[crate::db::HistoryEntry], ) -> Result<()> { let mut req = CiphersPutReq { - ty: 1, + 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(), @@ -949,8 +1155,8 @@ impl Client { } let client = reqwest::blocking::Client::new(); let res = client - .put(&self.api_url(&format!("/ciphers/{}", id))) - .header("Authorization", format!("Bearer {}", access_token)) + .put(self.api_url(&format!("/ciphers/{id}"))) + .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; @@ -968,8 +1174,8 @@ impl Client { 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)) + .delete(self.api_url(&format!("/ciphers/{id}"))) + .header("Authorization", format!("Bearer {access_token}")) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { @@ -989,8 +1195,8 @@ impl Client { ) -> Result<Vec<(String, String)>> { let client = reqwest::blocking::Client::new(); let res = client - .get(&self.api_url("/folders")) - .header("Authorization", format!("Bearer {}", access_token)) + .get(self.api_url("/folders")) + .header("Authorization", format!("Bearer {access_token}")) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { @@ -1021,8 +1227,8 @@ impl Client { }; let client = reqwest::blocking::Client::new(); let res = client - .post(&self.api_url("/folders")) - .header("Authorization", format!("Bearer {}", access_token)) + .post(self.api_url("/folders")) + .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; @@ -1051,7 +1257,7 @@ impl Client { }; let client = reqwest::blocking::Client::new(); let res = client - .post(&self.identity_url("/connect/token")) + .post(self.identity_url("/connect/token")) .form(&connect_req) .send() .map_err(|source| Error::Reqwest { source })?; @@ -1068,7 +1274,7 @@ impl Client { client_id: "desktop".to_string(), refresh_token: refresh_token.to_string(), }; - let client = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) diff --git a/src/base64.rs b/src/base64.rs new file mode 100644 index 0000000..86971bc --- /dev/null +++ b/src/base64.rs @@ -0,0 +1,15 @@ +use base64::Engine as _; + +pub fn encode<T: AsRef<[u8]>>(input: T) -> String { + base64::engine::general_purpose::STANDARD.encode(input) +} + +pub fn encode_url_safe_no_pad<T: AsRef<[u8]>>(input: T) -> String { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input) +} + +pub fn decode<T: AsRef<[u8]>>( + input: T, +) -> Result<Vec<u8>, base64::DecodeError> { + base64::engine::general_purpose::STANDARD.decode(input) +} diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs index 1cc71c3..ff39510 100644 --- a/src/bin/rbw-agent/actions.rs +++ b/src/bin/rbw-agent/actions.rs @@ -10,9 +10,7 @@ pub async fn register( let url_str = config_base_url().await?; let url = reqwest::Url::parse(&url_str) .context("failed to parse base url")?; - let host = if let Some(host) = url.host_str() { - host - } else { + let Some(host) = url.host_str() else { return Err(anyhow::anyhow!( "couldn't find host in rbw base url {}", url_str @@ -33,7 +31,7 @@ pub async fn register( let client_id = rbw::pinentry::getpin( &config_pinentry().await?, "API key client__id", - &format!("Log in to {}", host), + &format!("Log in to {host}"), err.as_deref(), tty, false, @@ -43,7 +41,7 @@ pub async fn register( let client_secret = rbw::pinentry::getpin( &config_pinentry().await?, "API key client__secret", - &format!("Log in to {}", host), + &format!("Log in to {host}"), err.as_deref(), tty, false, @@ -61,10 +59,9 @@ pub async fn register( message, }) .context("failed to log in to bitwarden instance"); - } else { - err_msg = Some(message); - continue; } + err_msg = Some(message); + continue; } Err(e) => { return Err(e) @@ -81,7 +78,7 @@ pub async fn register( pub async fn login( sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, tty: Option<&str>, ) -> anyhow::Result<()> { let db = load_db().await.unwrap_or_else(|_| rbw::db::Db::new()); @@ -90,9 +87,7 @@ pub async fn login( let url_str = config_base_url().await?; let url = reqwest::Url::parse(&url_str) .context("failed to parse base url")?; - let host = if let Some(host) = url.host_str() { - host - } else { + let Some(host) = url.host_str() else { return Err(anyhow::anyhow!( "couldn't find host in rbw base url {}", url_str @@ -102,7 +97,7 @@ pub async fn login( let email = config_email().await?; let mut err_msg = None; - for i in 1_u8..=3 { + '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 @@ -113,7 +108,7 @@ pub async fn login( let password = rbw::pinentry::getpin( &config_pinentry().await?, "Master Password", - &format!("Log in to {}", host), + &format!("Log in to {host}"), err.as_deref(), tty, true, @@ -126,55 +121,70 @@ pub async fn login( Ok(( access_token, refresh_token, + kdf, iterations, + memory, + parallelism, protected_key, )) => { login_success( - sock, - state, + state.clone(), access_token, refresh_token, + kdf, iterations, + memory, + parallelism, protected_key, password, db, email, ) .await?; - break; + break 'attempts; } Err(rbw::error::Error::TwoFactorRequired { providers }) => { - if providers.contains( - &rbw::api::TwoFactorProviderType::Authenticator, - ) { - let ( - access_token, - refresh_token, - iterations, - protected_key, - ) = two_factor( - tty, - &email, - password.clone(), - rbw::api::TwoFactorProviderType::Authenticator, - ) - .await?; - login_success( - sock, - state, - access_token, - refresh_token, - iterations, - protected_key, - password, - db, - email, - ) - .await?; - break; - } else { - return Err(anyhow::anyhow!("TODO")); + 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( + tty, + &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!("TODO")); } Err(rbw::error::Error::IncorrectPassword { message }) => { if i == 3 { @@ -182,10 +192,9 @@ pub async fn login( message, }) .context("failed to log in to bitwarden instance"); - } else { - err_msg = Some(message); - continue; } + err_msg = Some(message); + continue; } Err(e) => { return Err(e) @@ -205,7 +214,15 @@ async fn two_factor( email: &str, password: rbw::locked::Password, provider: rbw::api::TwoFactorProviderType, -) -> anyhow::Result<(String, String, u32, String)> { +) -> anyhow::Result<( + String, + String, + rbw::api::KdfType, + u32, + Option<u32>, + Option<u32>, + String, +)> { let mut err_msg = None; for i in 1_u8..=3 { let err = if i > 1 { @@ -217,11 +234,11 @@ async fn two_factor( }; let code = rbw::pinentry::getpin( &config_pinentry().await?, - "Authenticator App", - "Enter the 6 digit verification code from your authenticator app.", + provider.header(), + provider.message(), err.as_deref(), tty, - true, + provider.grab(), ) .await .context("failed to read code from pinentry")?; @@ -235,11 +252,22 @@ async fn two_factor( ) .await { - Ok((access_token, refresh_token, iterations, protected_key)) => { + Ok(( + access_token, + refresh_token, + kdf, + iterations, + memory, + parallelism, + protected_key, + )) => { return Ok(( access_token, refresh_token, + kdf, iterations, + memory, + parallelism, protected_key, )) } @@ -249,10 +277,9 @@ async fn two_factor( message, }) .context("failed to log in to bitwarden instance"); - } else { - err_msg = Some(message); - continue; } + err_msg = Some(message); + continue; } // can get this if the user passes an empty string Err(rbw::error::Error::TwoFactorRequired { .. }) => { @@ -262,10 +289,9 @@ async fn two_factor( message, }) .context("failed to log in to bitwarden instance"); - } else { - err_msg = Some(message); - continue; } + err_msg = Some(message); + continue; } Err(e) => { return Err(e) @@ -278,11 +304,13 @@ async fn two_factor( } async fn login_success( - sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, access_token: String, refresh_token: String, + kdf: rbw::api::KdfType, iterations: u32, + memory: Option<u32>, + parallelism: Option<u32>, protected_key: String, password: rbw::locked::Password, mut db: rbw::db::Db, @@ -290,35 +318,37 @@ async fn login_success( ) -> 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(sock, false).await?; + sync(None, state.clone()).await?; let db = load_db().await?; - let protected_private_key = - if let Some(protected_private_key) = db.protected_private_key { - protected_private_key - } else { - return Err(anyhow::anyhow!( - "failed to find protected private 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 res = rbw::actions::unlock( &email, &password, + kdf, iterations, + memory, + parallelism, &protected_key, &protected_private_key, &db.protected_org_keys, - ) - .await; + ); match res { Ok((keys, org_keys)) => { - let mut state = state.write().await; + let mut state = state.lock().await; state.priv_key = Some(keys); state.org_keys = Some(org_keys); } @@ -330,39 +360,40 @@ async fn login_success( pub async fn unlock( sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, tty: Option<&str>, ) -> anyhow::Result<()> { - if state.read().await.needs_unlock() { + if state.lock().await.needs_unlock() { let db = load_db().await?; - let iterations = if let Some(iterations) = db.iterations { - iterations - } else { + 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 protected_key = if let Some(protected_key) = db.protected_key { - protected_key - } else { + + 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 protected_private_key = - if let Some(protected_private_key) = db.protected_private_key { - protected_private_key - } else { - return Err(anyhow::anyhow!( - "failed to find protected private 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 1u8..=3 { + 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 @@ -373,7 +404,10 @@ pub async fn unlock( let password = rbw::pinentry::getpin( &config_pinentry().await?, "Master Password", - "Unlock the local database", + &format!( + "Unlock the local database for '{}'", + rbw::dirs::profile() + ), err.as_deref(), tty, true, @@ -383,13 +417,14 @@ pub async fn unlock( match rbw::actions::unlock( &email, &password, + kdf, iterations, + memory, + parallelism, &protected_key, &protected_private_key, &db.protected_org_keys, - ) - .await - { + ) { Ok((keys, org_keys)) => { unlock_success(state, keys, org_keys).await?; break; @@ -400,10 +435,9 @@ pub async fn unlock( message, }) .context("failed to unlock database"); - } else { - err_msg = Some(message); - continue; } + err_msg = Some(message); + continue; } Err(e) => return Err(e).context("failed to unlock database"), } @@ -416,11 +450,11 @@ pub async fn unlock( } async fn unlock_success( - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, keys: rbw::locked::Keys, org_keys: std::collections::HashMap<String, rbw::locked::Keys>, ) -> anyhow::Result<()> { - let mut state = state.write().await; + let mut state = state.lock().await; state.priv_key = Some(keys); state.org_keys = Some(org_keys); Ok(()) @@ -428,9 +462,9 @@ async fn unlock_success( pub async fn lock( sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, ) -> anyhow::Result<()> { - state.write().await.clear(); + state.lock().await.clear(); respond_ack(sock).await?; @@ -439,10 +473,10 @@ pub async fn lock( pub async fn check_lock( sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, _tty: Option<&str>, ) -> anyhow::Result<()> { - if state.read().await.needs_unlock() { + if state.lock().await.needs_unlock() { return Err(anyhow::anyhow!("agent is locked")); } @@ -452,8 +486,8 @@ pub async fn check_lock( } pub async fn sync( - sock: &mut crate::sock::Sock, - ack: bool, + sock: Option<&mut crate::sock::Sock>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, ) -> anyhow::Result<()> { let mut db = load_db().await?; @@ -482,7 +516,11 @@ pub async fn sync( db.entries = entries; save_db(&db).await?; - if ack { + 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?; } @@ -491,14 +529,12 @@ pub async fn sync( pub async fn decrypt( sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, cipherstring: &str, org_id: Option<&str>, ) -> anyhow::Result<()> { - let state = state.read().await; - let keys = if let Some(keys) = state.key(org_id) { - keys - } else { + 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" )); @@ -519,14 +555,12 @@ pub async fn decrypt( pub async fn encrypt( sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, plaintext: &str, org_id: Option<&str>, ) -> anyhow::Result<()> { - let state = state.read().await; - let keys = if let Some(keys) = state.key(org_id) { - keys - } else { + 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" )); @@ -542,6 +576,25 @@ pub async fn encrypt( Ok(()) } +pub async fn clipboard_store( + sock: &mut crate::sock::Sock, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, + text: &str, +) -> anyhow::Result<()> { + state + .lock() + .await + .clipboard + .set_contents(text.to_owned()) + .map_err(|e| { + anyhow::anyhow!("couldn't store value to clipboard: {e}") + })?; + + respond_ack(sock).await?; + + Ok(()) +} + pub async fn version(sock: &mut crate::sock::Sock) -> anyhow::Result<()> { sock.send(&rbw::protocol::Response::Version { version: rbw::protocol::version(), @@ -579,11 +632,10 @@ async fn respond_encrypt( async fn config_email() -> anyhow::Result<String> { let config = rbw::config::Config::load_async().await?; - if let Some(email) = config.email { - Ok(email) - } else { - Err(anyhow::anyhow!("failed to find email address in config")) - } + config.email.map_or_else( + || Err(anyhow::anyhow!("failed to find email address in config")), + Ok, + ) } async fn load_db() -> anyhow::Result<rbw::db::Db> { @@ -617,3 +669,35 @@ async fn config_pinentry() -> anyhow::Result<String> { let config = rbw::config::Config::load_async().await?; Ok(config.pinentry) } + +pub async fn subscribe_to_notifications( + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, +) -> 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()))) +} diff --git a/src/bin/rbw-agent/agent.rs b/src/bin/rbw-agent/agent.rs index fae8c7b..1345967 100644 --- a/src/bin/rbw-agent/agent.rs +++ b/src/bin/rbw-agent/agent.rs @@ -1,24 +1,25 @@ use anyhow::Context as _; +use futures_util::StreamExt as _; -#[derive(Debug)] -pub enum TimeoutEvent { - Set, - Clear, -} +use crate::notifications; pub struct State { pub priv_key: Option<rbw::locked::Keys>, pub org_keys: Option<std::collections::HashMap<String, rbw::locked::Keys>>, - pub timeout_chan: tokio::sync::mpsc::UnboundedSender<TimeoutEvent>, + 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, + pub clipboard: Box<dyn copypasta::ClipboardProvider>, } impl State { pub fn key(&self, org_id: Option<&str>) -> Option<&rbw::locked::Keys> { - match org_id { - Some(id) => self.org_keys.as_ref().and_then(|h| h.get(id)), - None => self.priv_key.as_ref(), - } + 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 { @@ -26,103 +27,163 @@ impl State { } pub fn set_timeout(&mut self) { - // no real better option to unwrap here - self.timeout_chan.send(TimeoutEvent::Set).unwrap(); + self.timeout.set(self.timeout_duration); } pub fn clear(&mut self) { self.priv_key = None; - self.org_keys = Default::default(); - // no real better option to unwrap here - self.timeout_chan.send(TimeoutEvent::Clear).unwrap(); + self.org_keys = None; + self.timeout.clear(); + } + + pub fn set_sync_timeout(&mut self) { + self.sync_timeout.set(self.sync_timeout_duration); } } pub struct Agent { - timeout_duration: tokio::time::Duration, - timeout: Option<std::pin::Pin<Box<tokio::time::Sleep>>>, - timeout_chan: tokio::sync::mpsc::UnboundedReceiver<TimeoutEvent>, - state: std::sync::Arc<tokio::sync::RwLock<State>>, + timer_r: tokio::sync::mpsc::UnboundedReceiver<()>, + sync_timer_r: tokio::sync::mpsc::UnboundedReceiver<()>, + state: std::sync::Arc<tokio::sync::Mutex<State>>, } impl Agent { pub fn new() -> anyhow::Result<Self> { let config = rbw::config::Config::load()?; let timeout_duration = - tokio::time::Duration::from_secs(config.lock_timeout); - let (w, r) = tokio::sync::mpsc::unbounded_channel(); + 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(); + let clipboard: Box<dyn copypasta::ClipboardProvider> = + copypasta::ClipboardContext::new().map_or_else( + |e| { + log::warn!("couldn't create clipboard context: {e}"); + let clipboard = Box::new( + // infallible + copypasta::nop_clipboard::NopClipboardContext::new() + .unwrap(), + ); + let clipboard: Box<dyn copypasta::ClipboardProvider> = + clipboard; + clipboard + }, + |clipboard| { + let clipboard = Box::new(clipboard); + let clipboard: Box<dyn copypasta::ClipboardProvider> = + clipboard; + clipboard + }, + ); Ok(Self { - timeout_duration, - timeout: None, - timeout_chan: r, - state: std::sync::Arc::new(tokio::sync::RwLock::new(State { + timer_r, + sync_timer_r, + state: std::sync::Arc::new(tokio::sync::Mutex::new(State { priv_key: None, - org_keys: Default::default(), - timeout_chan: w, + org_keys: None, + timeout, + timeout_duration, + sync_timeout, + sync_timeout_duration, + notifications_handler, + clipboard, })), }) } - fn set_timeout(&mut self) { - self.timeout = - Some(Box::pin(tokio::time::sleep(self.timeout_duration))); - } - - fn clear_timeout(&mut self) { - self.timeout = None; - } - pub async fn run( - &mut self, + self, listener: tokio::net::UnixListener, ) -> anyhow::Result<()> { - // tokio only supports timeouts up to 2^36 milliseconds - let mut forever = Box::pin(tokio::time::sleep( - tokio::time::Duration::from_secs(60 * 60 * 24 * 365 * 2), - )); - loop { - let timeout = if let Some(timeout) = &mut self.timeout { - timeout - } else { - &mut forever - }; - tokio::select! { - sock = listener.accept() => { + pub enum Event { + Request(std::io::Result<tokio::net::UnixStream>), + 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 { + notifications::Message::Logout => Event::Timeout(()), + 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( - sock.context("failed to accept incoming connection")?.0 + 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; + 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(); + error: format!("{e:#}"), + }) + .await + .unwrap(); } }); } - _ = timeout => { + Event::Timeout(()) => { + self.state.lock().await.clear(); + } + Event::Sync(()) => { let state = self.state.clone(); - tokio::spawn(async move{ - state.write().await.clear(); + 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:#}"); + } }); - } - Some(ev) = self.timeout_chan.recv() => { - match ev { - TimeoutEvent::Set => self.set_timeout(), - TimeoutEvent::Clear => self.clear_timeout(), - } + self.state.lock().await.set_sync_timeout(); } } } + Ok(()) } } async fn handle_request( sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<State>>, + state: std::sync::Arc<tokio::sync::Mutex<State>>, ) -> anyhow::Result<()> { let req = sock.recv().await?; let req = match req { @@ -161,7 +222,7 @@ async fn handle_request( false } rbw::protocol::Action::Sync => { - crate::actions::sync(sock, true).await?; + crate::actions::sync(Some(sock), state.clone()).await?; false } rbw::protocol::Action::Decrypt { @@ -187,6 +248,11 @@ async fn handle_request( .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?; @@ -195,7 +261,7 @@ async fn handle_request( }; if set_timeout { - state.write().await.set_timeout(); + state.lock().await.set_timeout(); } Ok(()) diff --git a/src/bin/rbw-agent/daemon.rs b/src/bin/rbw-agent/daemon.rs index 923a217..8cb9998 100644 --- a/src/bin/rbw-agent/daemon.rs +++ b/src/bin/rbw-agent/daemon.rs @@ -30,32 +30,37 @@ pub fn daemonize() -> anyhow::Result<StartupAck> { .open(rbw::dirs::agent_stderr_file())?; let (r, w) = nix::unistd::pipe()?; - let res = daemonize::Daemonize::new() + let daemonize = daemonize::Daemonize::new() .pid_file(rbw::dirs::pid_file()) .stdout(stdout) - .stderr(stderr) - .exit_action(move || { + .stderr(stderr); + let res = match daemonize.execute() { + daemonize::Outcome::Parent(_) => { // unwraps are necessary because not really a good way to handle // errors here otherwise let _ = nix::unistd::close(w); let mut buf = [0; 1]; nix::unistd::read(r, &mut buf).unwrap(); nix::unistd::close(r).unwrap(); - }) - .start(); + std::process::exit(0); + } + daemonize::Outcome::Child(res) => res, + }; + let _ = nix::unistd::close(r); match res { Ok(_) => (), Err(e) => { - match e { - daemonize::DaemonizeError::LockPidfile(_) => { - // 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); - } - _ => panic!("failed to daemonize: {}", 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}"); } } } diff --git a/src/bin/rbw-agent/main.rs b/src/bin/rbw-agent/main.rs index 69411ae..8434c72 100644 --- a/src/bin/rbw-agent/main.rs +++ b/src/bin/rbw-agent/main.rs @@ -1,4 +1,19 @@ +#![warn(clippy::cargo)] +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![warn(clippy::as_conversions)] +#![warn(clippy::get_unwrap)] +#![allow(clippy::cognitive_complexity)] +#![allow(clippy::missing_const_for_fn)] +#![allow(clippy::similar_names)] +#![allow(clippy::struct_excessive_bools)] #![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::type_complexity)] +#![allow(clippy::multiple_crate_versions)] +#![allow(clippy::large_enum_variant)] +// this one looks plausibly useful, but currently has too many bugs +#![allow(clippy::significant_drop_tightening)] use anyhow::Context as _; @@ -6,7 +21,9 @@ mod actions; mod agent; mod daemon; mod debugger; +mod notifications; mod sock; +mod timeout; async fn tokio_main( startup_ack: Option<crate::daemon::StartupAck>, @@ -17,7 +34,7 @@ async fn tokio_main( startup_ack.ack()?; } - let mut agent = crate::agent::Agent::new()?; + let agent = crate::agent::Agent::new()?; agent.run(listener).await?; Ok(()) @@ -29,11 +46,9 @@ fn real_main() -> anyhow::Result<()> { ) .init(); - let no_daemonize = if let Some(arg) = std::env::args().nth(1) { - arg == "--no-daemonize" - } else { - false - }; + let no_daemonize = std::env::args() + .nth(1) + .map_or(false, |arg| arg == "--no-daemonize"); let startup_ack = if no_daemonize { None @@ -69,7 +84,7 @@ fn main() { if let Err(e) = res { // XXX log file? - eprintln!("{:#}", e); + eprintln!("{e:#}"); std::process::exit(1); } } diff --git a/src/bin/rbw-agent/notifications.rs b/src/bin/rbw-agent/notifications.rs new file mode 100644 index 0000000..8176603 --- /dev/null +++ b/src/bin/rbw-agent/notifications.rs @@ -0,0 +1,174 @@ +use 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::net::TcpStream>, + >, + tokio_tungstenite::tungstenite::Message, + >, + >, + read_handle: Option<tokio::task::JoinHandle<()>>, + sending_channels: std::sync::Arc< + tokio::sync::RwLock<Vec<tokio::sync::mpsc::UnboundedSender<Message>>>, + >, +} + +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<dyn std::error::Error>> { + 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<dyn std::error::Error>> { + 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( + &mut self, + ) -> tokio::sync::mpsc::UnboundedReceiver<Message> { + 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<Vec<tokio::sync::mpsc::UnboundedSender<Message>>>, + >, +) -> Result< + ( + futures_util::stream::SplitSink< + tokio_tungstenite::WebSocketStream< + tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>, + >, + tokio_tungstenite::tungstenite::Message, + >, + tokio::task::JoinHandle<()>, + ), + Box<dyn std::error::Error>, +> { + 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<Message> { + 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 +} diff --git a/src/bin/rbw-agent/sock.rs b/src/bin/rbw-agent/sock.rs index 311176c..280b8cc 100644 --- a/src/bin/rbw-agent/sock.rs +++ b/src/bin/rbw-agent/sock.rs @@ -36,9 +36,8 @@ impl Sock { 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) - })) + Ok(serde_json::from_str(&line) + .map_err(|e| format!("failed to parse message '{line}': {e}"))) } } diff --git a/src/bin/rbw-agent/timeout.rs b/src/bin/rbw-agent/timeout.rs new file mode 100644 index 0000000..e2aba06 --- /dev/null +++ b/src/bin/rbw-agent/timeout.rs @@ -0,0 +1,66 @@ +use 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<Action>, +} + +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(); + } +} diff --git a/src/bin/rbw/actions.rs b/src/bin/rbw/actions.rs index 39fde15..e0e7a2e 100644 --- a/src/bin/rbw/actions.rs +++ b/src/bin/rbw/actions.rs @@ -31,11 +31,11 @@ pub fn quit() -> anyhow::Result<()> { let pidfile = rbw::dirs::pid_file(); let mut pid = String::new(); std::fs::File::open(pidfile)?.read_to_string(&mut pid)?; - let pid = nix::unistd::Pid::from_raw(pid.parse()?); + let pid = nix::unistd::Pid::from_raw(pid.trim_end().parse()?); sock.send(&rbw::protocol::Request { - tty: nix::unistd::ttyname(0) - .ok() - .and_then(|p| p.to_str().map(|s| s.to_string())), + tty: nix::unistd::ttyname(0).ok().and_then(|p| { + p.to_str().map(std::string::ToString::to_string) + }), action: rbw::protocol::Action::Quit, })?; wait_for_exit(pid); @@ -59,7 +59,7 @@ pub fn decrypt( sock.send(&rbw::protocol::Request { tty: nix::unistd::ttyname(0) .ok() - .and_then(|p| p.to_str().map(|s| s.to_string())), + .and_then(|p| p.to_str().map(std::string::ToString::to_string)), action: rbw::protocol::Action::Decrypt { cipherstring: cipherstring.to_string(), org_id: org_id.map(std::string::ToString::to_string), @@ -84,7 +84,7 @@ pub fn encrypt( sock.send(&rbw::protocol::Request { tty: nix::unistd::ttyname(0) .ok() - .and_then(|p| p.to_str().map(|s| s.to_string())), + .and_then(|p| p.to_str().map(std::string::ToString::to_string)), action: rbw::protocol::Action::Encrypt { plaintext: plaintext.to_string(), org_id: org_id.map(std::string::ToString::to_string), @@ -101,12 +101,18 @@ pub fn encrypt( } } +pub fn clipboard_store(text: &str) -> anyhow::Result<()> { + simple_action(rbw::protocol::Action::ClipboardStore { + text: text.to_string(), + }) +} + pub fn version() -> anyhow::Result<u32> { let mut sock = connect()?; sock.send(&rbw::protocol::Request { tty: nix::unistd::ttyname(0) .ok() - .and_then(|p| p.to_str().map(|s| s.to_string())), + .and_then(|p| p.to_str().map(std::string::ToString::to_string)), action: rbw::protocol::Action::Version, })?; @@ -126,7 +132,7 @@ fn simple_action(action: rbw::protocol::Action) -> anyhow::Result<()> { sock.send(&rbw::protocol::Request { tty: nix::unistd::ttyname(0) .ok() - .and_then(|p| p.to_str().map(|s| s.to_string())), + .and_then(|p| p.to_str().map(std::string::ToString::to_string)), action, })?; diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 9efd966..a4c0a26 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -1,4 +1,7 @@ use anyhow::Context as _; +use serde::Serialize; +use std::io; +use std::io::prelude::Write; const MISSING_CONFIG_HELP: &str = "Before using rbw, you must configure the email address you would like to \ @@ -10,7 +13,7 @@ const MISSING_CONFIG_HELP: &str = and, if your server has a non-default identity url:\n\n \ rbw config set identity_url <url>\n"; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedCipher { id: String, @@ -23,25 +26,25 @@ struct DecryptedCipher { } impl DecryptedCipher { - fn display_short(&self, desc: &str) -> bool { + fn display_short(&self, desc: &str, clipboard: bool) -> bool { match &self.data { DecryptedData::Login { password, .. } => { - if let Some(password) = password { - println!("{}", password); - true - } else { - eprintln!("entry for '{}' had no password", desc); - false - } + password.as_ref().map_or_else( + || { + eprintln!("entry for '{desc}' had no password"); + false + }, + |password| val_display_or_store(clipboard, password), + ) } DecryptedData::Card { number, .. } => { - if let Some(number) = number { - println!("{}", number); - true - } else { - eprintln!("entry for '{}' had no card number", desc); - false - } + 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, @@ -54,30 +57,272 @@ impl DecryptedCipher { [title, first_name, middle_name, last_name] .iter() .copied() - .cloned() .flatten() + .cloned() .collect(); if names.is_empty() { - eprintln!("entry for '{}' had no name", desc); + eprintln!("entry for '{desc}' had no name"); false } else { - println!("{}", names.join(" ")); - true + val_display_or_store(clipboard, &names.join(" ")) } } - DecryptedData::SecureNote {} => { - if let Some(notes) = &self.notes { - println!("{}", notes); - true - } else { - eprintln!("entry for '{}' had no notes", desc); + 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) { + fn display_long(&self, desc: &str, clipboard: bool) { match &self.data { DecryptedData::Login { username, @@ -85,29 +330,31 @@ impl DecryptedCipher { uris, .. } => { - let mut displayed = self.display_short(desc); + let mut displayed = self.display_short(desc, clipboard); displayed |= - self.display_field("Username", username.as_deref()); + display_field("Username", username.as_deref(), clipboard); displayed |= - self.display_field("TOTP Secret", totp.as_deref()); + display_field("TOTP Secret", totp.as_deref(), clipboard); if let Some(uris) = uris { for uri in uris { displayed |= - self.display_field("URI", Some(&uri.uri)); + display_field("URI", Some(&uri.uri), clipboard); let match_type = - uri.match_type.map(|ty| format!("{}", ty)); - displayed |= self.display_field( + uri.match_type.map(|ty| format!("{ty}")); + displayed |= display_field( "Match type", match_type.as_deref(), + clipboard, ); } } for field in &self.fields { - displayed |= self.display_field( + displayed |= display_field( field.name.as_deref().unwrap_or("(null)"), Some(field.value.as_deref().unwrap_or("")), + clipboard, ); } @@ -115,7 +362,7 @@ impl DecryptedCipher { if displayed { println!(); } - println!("{}", notes); + println!("{notes}"); } } DecryptedData::Card { @@ -126,24 +373,28 @@ impl DecryptedCipher { code, .. } => { - let mut displayed = self.display_short(desc); + 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); + println!("Expiration: {exp_month}/{exp_year}"); displayed = true; } - displayed |= self.display_field("CVV", code.as_deref()); + displayed |= display_field("CVV", code.as_deref(), clipboard); + displayed |= display_field( + "Name", + cardholder_name.as_deref(), + clipboard, + ); displayed |= - self.display_field("Name", cardholder_name.as_deref()); - displayed |= self.display_field("Brand", brand.as_deref()); + display_field("Brand", brand.as_deref(), clipboard); if let Some(notes) = &self.notes { if displayed { println!(); } - println!("{}", notes); + println!("{notes}"); } } DecryptedData::Identity { @@ -162,65 +413,76 @@ impl DecryptedCipher { username, .. } => { - let mut displayed = self.display_short(desc); + let mut displayed = self.display_short(desc, clipboard); displayed |= - self.display_field("Address", address1.as_deref()); + display_field("Address", address1.as_deref(), clipboard); + displayed |= + display_field("Address", address2.as_deref(), clipboard); displayed |= - self.display_field("Address", address2.as_deref()); + display_field("Address", address3.as_deref(), clipboard); displayed |= - self.display_field("Address", address3.as_deref()); - displayed |= self.display_field("City", city.as_deref()); - displayed |= self.display_field("State", state.as_deref()); + display_field("City", city.as_deref(), clipboard); displayed |= - self.display_field("Postcode", postal_code.as_deref()); + display_field("State", state.as_deref(), clipboard); + displayed |= display_field( + "Postcode", + postal_code.as_deref(), + clipboard, + ); displayed |= - self.display_field("Country", country.as_deref()); - displayed |= self.display_field("Phone", phone.as_deref()); - displayed |= self.display_field("Email", email.as_deref()); - displayed |= self.display_field("SSN", ssn.as_deref()); + display_field("Country", country.as_deref(), clipboard); displayed |= - self.display_field("License", license_number.as_deref()); - displayed |= self - .display_field("Passport", passport_number.as_deref()); + display_field("Phone", phone.as_deref(), clipboard); displayed |= - self.display_field("Username", username.as_deref()); + 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); + println!("{notes}"); } } DecryptedData::SecureNote {} => { - self.display_short(desc); + self.display_short(desc, clipboard); } } } - fn display_field(&self, name: &str, field: Option<&str>) -> bool { - if let Some(field) = field { - println!("{}: {}", name, field); - true - } else { - false - } - } - fn display_name(&self) -> String { match &self.data { DecryptedData::Login { username, .. } => { - if let Some(username) = username { - format!("{}@{}", username, self.name) - } else { - self.name.clone() - } + 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, name: &str, @@ -312,7 +574,23 @@ impl DecryptedCipher { } } -#[derive(Debug, Clone)] +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, Serialize)] +#[serde(untagged)] #[cfg_attr(test, derive(Eq, PartialEq))] enum DecryptedData { Login { @@ -351,21 +629,21 @@ enum DecryptedData { SecureNote, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedField { name: Option<String>, value: Option<String>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedHistoryEntry { last_used_date: String, password: String, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedUri { uri: String, @@ -393,11 +671,16 @@ impl std::convert::TryFrom<&String> for ListField { } } -const HELP: &str = r#" +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()?; @@ -415,6 +698,13 @@ pub fn config_set(key: &str, value: &str) -> anyhow::Result<()> { "email" => config.email = Some(value.to_string()), "base_url" => config.base_url = Some(value.to_string()), "identity_url" => config.identity_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() @@ -425,6 +715,12 @@ pub fn config_set(key: &str, value: &str) -> anyhow::Result<()> { 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)), } @@ -447,8 +743,10 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> { "email" => config.email = None, "base_url" => config.base_url = None, "identity_url" => config.identity_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() + config.lock_timeout = rbw::config::default_lock_timeout(); } "pinentry" => config.pinentry = rbw::config::default_pinentry(), _ => return Err(anyhow::anyhow!("invalid config key: {}", key)), @@ -465,6 +763,13 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> { 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()?; @@ -514,8 +819,7 @@ pub fn list(fields: &[String]) -> anyhow::Result<()> { let mut ciphers: Vec<DecryptedCipher> = db .entries .iter() - .cloned() - .map(|entry| decrypt_cipher(&entry)) + .map(decrypt_cipher) .collect::<anyhow::Result<_>>()?; ciphers.sort_unstable_by(|a, b| a.name.cmp(&b.name)); @@ -526,20 +830,27 @@ pub fn list(fields: &[String]) -> anyhow::Result<()> { ListField::Name => cipher.name.clone(), ListField::Id => cipher.id.clone(), ListField::User => match &cipher.data { - DecryptedData::Login { username, .. } => username - .as_ref() - .map(std::string::ToString::to_string) - .unwrap_or_else(|| "".to_string()), - _ => "".to_string(), + 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(std::string::ToString::to_string) - .unwrap_or_else(|| "".to_string()), + ListField::Folder => cipher.folder.as_ref().map_or_else( + String::new, + std::string::ToString::to_string, + ), }) .collect(); - println!("{}", values.join("\t")); + + // write to stdout but don't panic when pipe get's closed + // this happens when piping stdout in a shell + match writeln!(&mut io::stdout(), "{}", values.join("\t")) { + Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()), + res => res, + }?; } Ok(()) @@ -549,7 +860,10 @@ pub fn get( name: &str, user: Option<&str>, folder: Option<&str>, + field: Option<&str>, full: bool, + raw: bool, + clipboard: bool, ) -> anyhow::Result<()> { unlock()?; @@ -557,17 +871,20 @@ pub fn get( let desc = format!( "{}{}", - user.map(|s| format!("{}@", s)) - .unwrap_or_else(|| "".to_string()), + user.map_or_else(String::new, |s| format!("{s}@")), name ); let (_, decrypted) = find_entry(&db, name, user, folder) - .with_context(|| format!("couldn't find entry for '{}'", desc))?; - if full { - decrypted.display_long(&desc); + .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); + decrypted.display_short(&desc, clipboard); } Ok(()) @@ -584,17 +901,16 @@ pub fn code( let desc = format!( "{}{}", - user.map(|s| format!("{}@", s)) - .unwrap_or_else(|| "".to_string()), + user.map_or_else(String::new, |s| format!("{s}@")), name ); let (_, decrypted) = find_entry(&db, name, user, folder) - .with_context(|| format!("couldn't find entry for '{}'", desc))?; + .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let DecryptedData::Login { totp, .. } = decrypted.data { if let Some(totp) = totp { - println!("{}", generate_totp(&totp)?) + println!("{}", generate_totp(&totp)?); } else { return Err(anyhow::anyhow!( "entry does not contain a totp secret" @@ -610,7 +926,7 @@ pub fn code( pub fn add( name: &str, username: Option<&str>, - uris: Vec<(String, Option<rbw::api::UriMatchType>)>, + uris: &[(String, Option<rbw::api::UriMatchType>)], folder: Option<&str>, ) -> anyhow::Result<()> { unlock()?; @@ -627,7 +943,7 @@ pub fn add( .map(|username| crate::actions::encrypt(username, None)) .transpose()?; - let contents = rbw::edit::edit("", HELP)?; + let contents = rbw::edit::edit("", HELP_PW)?; let (password, notes) = parse_editor(&contents); let password = password @@ -707,13 +1023,13 @@ pub fn add( pub fn generate( name: Option<&str>, username: Option<&str>, - uris: Vec<(String, Option<rbw::api::UriMatchType>)>, + uris: &[(String, Option<rbw::api::UriMatchType>)], folder: Option<&str>, len: usize, ty: rbw::pwgen::Type, ) -> anyhow::Result<()> { let password = rbw::pwgen::pwgen(ty, len); - println!("{}", password); + println!("{password}"); if let Some(name) = name { unlock()?; @@ -813,24 +1129,22 @@ pub fn edit( let desc = format!( "{}{}", - username - .map(|s| format!("{}@", s)) - .unwrap_or_else(|| "".to_string()), + username.map_or_else(String::new, |s| format!("{s}@")), name ); let (entry, decrypted) = find_entry(&db, name, username, folder) - .with_context(|| format!("couldn't find entry for '{}'", desc))?; + .with_context(|| format!("couldn't find entry for '{desc}'"))?; let (data, 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{}\n", notes)); + contents.push_str(&format!("\n{notes}\n")); } - let contents = rbw::edit::edit(&contents, HELP)?; + let contents = rbw::edit::edit(&contents, HELP_NOTES)?; let (password, notes) = parse_editor(&contents); let password = password @@ -847,16 +1161,15 @@ pub fn edit( }) .transpose()?; let mut history = entry.history.clone(); - let (entry_username, entry_password, entry_uris, entry_totp) = - match &entry.data { - rbw::db::EntryData::Login { - username, - password, - uris, - totp, - } => (username, password, uris, totp), - _ => unreachable!(), - }; + 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 { @@ -874,14 +1187,34 @@ pub fn edit( let data = rbw::db::EntryData::Login { username: entry_username.clone(), password, - uris: entry_uris.to_vec(), + uris: entry_uris.clone(), totp: entry_totp.clone(), }; (data, 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, notes, entry.history) + } _ => { return Err(anyhow::anyhow!( - "modifications are only supported for login entries" + "modifications are only supported for login and note entries" )); } }; @@ -918,14 +1251,12 @@ pub fn remove( let desc = format!( "{}{}", - username - .map(|s| format!("{}@", s)) - .unwrap_or_else(|| "".to_string()), + username.map_or_else(String::new, |s| format!("{s}@")), name ); let (entry, _) = find_entry(&db, name, username, folder) - .with_context(|| format!("couldn't find entry for '{}'", desc))?; + .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let (Some(access_token), ()) = rbw::actions::remove(access_token, refresh_token, &entry.id)? @@ -950,14 +1281,12 @@ pub fn history( let desc = format!( "{}{}", - username - .map(|s| format!("{}@", s)) - .unwrap_or_else(|| "".to_string()), + username.map_or_else(String::new, |s| format!("{s}@")), name ); let (_, decrypted) = find_entry(&db, name, username, folder) - .with_context(|| format!("couldn't find entry for '{}'", desc))?; + .with_context(|| format!("couldn't find entry for '{desc}'"))?; for history in decrypted.history { println!("{}: {}", history.last_used_date, history.password); } @@ -1017,7 +1346,7 @@ fn ensure_agent_once() -> anyhow::Result<()> { let agent_path = std::env::var("RBW_AGENT"); let agent_path = agent_path .as_ref() - .map(|s| s.as_str()) + .map(std::string::String::as_str) .unwrap_or("rbw-agent"); let status = std::process::Command::new(agent_path) .status() @@ -1056,26 +1385,23 @@ fn find_entry( username: Option<&str>, folder: Option<&str>, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { - match uuid::Uuid::parse_str(name) { - Ok(_) => { - for cipher in &db.entries { - if name == cipher.id { - return Ok((cipher.clone(), decrypt_cipher(cipher)?)); - } + if uuid::Uuid::parse_str(name).is_ok() { + for cipher in &db.entries { + if name == cipher.id { + return Ok((cipher.clone(), decrypt_cipher(cipher)?)); } - Err(anyhow::anyhow!("no entry found")) - } - Err(_) => { - let ciphers: Vec<(rbw::db::Entry, DecryptedCipher)> = db - .entries - .iter() - .cloned() - .map(|entry| { - decrypt_cipher(&entry).map(|decrypted| (entry, decrypted)) - }) - .collect::<anyhow::Result<_>>()?; - find_entry_raw(&ciphers, name, username, folder) } + Err(anyhow::anyhow!("no entry found")) + } else { + let ciphers: Vec<(rbw::db::Entry, DecryptedCipher)> = db + .entries + .iter() + .cloned() + .map(|entry| { + decrypt_cipher(&entry).map(|decrypted| (entry, decrypted)) + }) + .collect::<anyhow::Result<_>>()?; + find_entry_raw(&ciphers, name, username, folder) } } @@ -1087,10 +1413,10 @@ fn find_entry_raw( ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { let mut matches: Vec<(rbw::db::Entry, DecryptedCipher)> = entries .iter() - .cloned() - .filter(|(_, decrypted_cipher)| { + .filter(|&(_, decrypted_cipher)| { decrypted_cipher.exact_match(name, username, folder, true) }) + .cloned() .collect(); if matches.len() == 1 { @@ -1100,10 +1426,10 @@ fn find_entry_raw( if folder.is_none() { matches = entries .iter() - .cloned() - .filter(|(_, decrypted_cipher)| { + .filter(|&(_, decrypted_cipher)| { decrypted_cipher.exact_match(name, username, folder, false) }) + .cloned() .collect(); if matches.len() == 1 { @@ -1113,10 +1439,10 @@ fn find_entry_raw( matches = entries .iter() - .cloned() - .filter(|(_, decrypted_cipher)| { + .filter(|&(_, decrypted_cipher)| { decrypted_cipher.partial_match(name, username, folder, true) }) + .cloned() .collect(); if matches.len() == 1 { @@ -1126,10 +1452,10 @@ fn find_entry_raw( if folder.is_none() { matches = entries .iter() - .cloned() - .filter(|(_, decrypted_cipher)| { + .filter(|&(_, decrypted_cipher)| { decrypted_cipher.partial_match(name, username, folder, false) }) + .cloned() .collect(); if matches.len() == 1 { return Ok(matches[0].clone()); @@ -1435,8 +1761,11 @@ fn parse_editor(contents: &str) -> (Option<String>, Option<String>) { let mut notes: String = lines .skip_while(|line| line.is_empty()) .filter(|line| !line.starts_with('#')) - .map(|line| format!("{}\n", line)) - .collect(); + .fold(String::new(), |mut notes, line| { + notes.push_str(line); + notes.push('\n'); + notes + }); while notes.ends_with('\n') { notes.pop(); } @@ -1447,32 +1776,35 @@ fn parse_editor(contents: &str) -> (Option<String>, Option<String>) { fn load_db() -> anyhow::Result<rbw::db::Db> { let config = rbw::config::Config::load()?; - if let Some(email) = &config.email { - rbw::db::Db::load(&config.server_name(), email) - .map_err(anyhow::Error::new) - } else { - Err(anyhow::anyhow!("failed to find email address in config")) - } + 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()?; - if let Some(email) = &config.email { - db.save(&config.server_name(), email) - .map_err(anyhow::Error::new) - } else { - Err(anyhow::anyhow!("failed to find email address in config")) - } + 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()?; - if let Some(email) = &config.email { - rbw::db::Db::remove(&config.server_name(), email) - .map_err(anyhow::Error::new) - } else { - Err(anyhow::anyhow!("failed to find email address in config")) - } + 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) + }, + ) } fn parse_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> { @@ -1500,7 +1832,7 @@ fn parse_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> { }; base32::decode( base32::Alphabet::RFC4648 { padding: false }, - &secret_str.replace(" ", ""), + &secret_str.replace(' ', ""), ) .ok_or_else(|| anyhow::anyhow!("totp secret was not valid base32")) } @@ -1517,6 +1849,13 @@ fn generate_totp(secret: &str) -> anyhow::Result<String> { )) } +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::*; @@ -1612,7 +1951,7 @@ mod test { ) -> bool { let res = find_entry_raw(entries, name, username, folder); if let Err(e) = res { - format!("{}", e).contains("no entry found") + format!("{e}").contains("no entry found") } else { false } @@ -1626,7 +1965,7 @@ mod test { ) -> bool { let res = find_entry_raw(entries, name, username, folder); if let Err(e) = res { - format!("{}", e).contains("multiple entries found") + format!("{e}").contains("multiple entries found") } else { false } diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 85631c5..72e4220 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -1,23 +1,36 @@ +#![warn(clippy::cargo)] +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![warn(clippy::as_conversions)] +#![warn(clippy::get_unwrap)] +#![allow(clippy::cognitive_complexity)] +#![allow(clippy::missing_const_for_fn)] +#![allow(clippy::similar_names)] +#![allow(clippy::struct_excessive_bools)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::type_complexity)] +#![allow(clippy::multiple_crate_versions)] #![allow(clippy::large_enum_variant)] use anyhow::Context as _; +use clap::{CommandFactory as _, Parser as _}; use std::io::Write as _; -use structopt::StructOpt as _; mod actions; mod commands; mod sock; -#[derive(Debug, structopt::StructOpt)] -#[structopt(about = "Unofficial Bitwarden CLI")] +#[derive(Debug, clap::Parser)] +#[command(version, about = "Unofficial Bitwarden CLI")] enum Opt { - #[structopt(about = "Get or set configuration options")] + #[command(about = "Get or set configuration options")] Config { - #[structopt(subcommand)] + #[command(subcommand)] config: Config, }, - #[structopt( + #[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 \ @@ -28,60 +41,63 @@ enum Opt { )] Register, - #[structopt(about = "Log in to the Bitwarden server")] + #[command(about = "Log in to the Bitwarden server")] Login, - #[structopt(about = "Unlock the local Bitwarden database")] + #[command(about = "Unlock the local Bitwarden database")] Unlock, - #[structopt(about = "Check if the local Bitwarden database is unlocked")] + #[command(about = "Check if the local Bitwarden database is unlocked")] Unlocked, - #[structopt(about = "Update the local copy of the Bitwarden database")] + #[command(about = "Update the local copy of the Bitwarden database")] Sync, - #[structopt( + #[command( about = "List all entries in the local Bitwarden database", visible_alias = "ls" )] List { - #[structopt( + #[arg( long, help = "Fields to display. \ Available options are id, name, user, folder. \ Multiple fields will be separated by tabs.", default_value = "name", - use_delimiter = true + use_value_delimiter = true )] fields: Vec<String>, }, - #[structopt(about = "Display the password for a given entry")] + #[command(about = "Display the password for a given entry")] Get { - #[structopt(help = "Name or UUID of the entry to display")] + #[arg(help = "Name or UUID of the entry to display")] name: String, - #[structopt(help = "Username of the entry to display")] + #[arg(help = "Username of the entry to display")] user: Option<String>, - #[structopt(long, help = "Folder name to search in")] + #[arg(long, help = "Folder name to search in")] folder: Option<String>, - #[structopt( - long, - help = "Display the notes in addition to the password" - )] + #[arg(short, long, help = "Field to get")] + field: Option<String>, + #[arg(long, help = "Display the notes in addition to the password")] full: bool, + #[structopt(long, help = "Display output as JSON")] + raw: bool, + #[structopt(long, help = "Copy result to clipboard")] + clipboard: bool, }, - #[structopt(about = "Display the authenticator code for a given entry")] + #[command(about = "Display the authenticator code for a given entry")] Code { - #[structopt(help = "Name or UUID of the entry to display")] + #[arg(help = "Name or UUID of the entry to display")] name: String, - #[structopt(help = "Username of the entry to display")] + #[arg(help = "Username of the entry to display")] user: Option<String>, - #[structopt(long, help = "Folder name to search in")] + #[arg(long, help = "Folder name to search in")] folder: Option<String>, }, - #[structopt( + #[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 \ @@ -91,28 +107,27 @@ enum Opt { remainder will be saved as a note." )] Add { - #[structopt(help = "Name of the password entry")] + #[arg(help = "Name of the password entry")] name: String, - #[structopt(help = "Username for the password entry")] + #[arg(help = "Username for the password entry")] user: Option<String>, - #[structopt( + #[arg( long, help = "URI for the password entry", - multiple = true, number_of_values = 1 )] uri: Vec<String>, - #[structopt(long, help = "Folder for the password entry")] + #[arg(long, help = "Folder for the password entry")] folder: Option<String>, }, - #[structopt( + #[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 = structopt::clap::ArgGroup::with_name("password-type").args(&[ + group = clap::ArgGroup::new("password-type").args(&[ "no-symbols", "only-numbers", "nonconfusables", @@ -120,39 +135,38 @@ enum Opt { ]) )] Generate { - #[structopt(help = "Length of the password to generate")] + #[arg(help = "Length of the password to generate")] len: usize, - #[structopt(help = "Name of the password entry")] + #[arg(help = "Name of the password entry")] name: Option<String>, - #[structopt(help = "Username for the password entry")] + #[arg(help = "Username for the password entry")] user: Option<String>, - #[structopt( + #[arg( long, help = "URI for the password entry", - multiple = true, number_of_values = 1 )] uri: Vec<String>, - #[structopt(long, help = "Folder for the password entry")] + #[arg(long, help = "Folder for the password entry")] folder: Option<String>, - #[structopt( + #[arg( long = "no-symbols", help = "Generate a password with no special characters" )] no_symbols: bool, - #[structopt( + #[arg( long = "only-numbers", help = "Generate a password consisting of only numbers" )] only_numbers: bool, - #[structopt( + #[arg( long, help = "Generate a password without visually similar \ characters (useful for passwords intended to be \ written down)" )] nonconfusables: bool, - #[structopt( + #[arg( long, help = "Generate a password of multiple dictionary \ words chosen from the EFF word list. The len \ @@ -162,7 +176,7 @@ enum Opt { diceware: bool, }, - #[structopt( + #[command( about = "Modify an existing password", long_about = "Modify an existing password\n\n\ This command will open a text editor with the existing \ @@ -173,50 +187,48 @@ enum Opt { as a note." )] Edit { - #[structopt(help = "Name or UUID of the password entry")] + #[arg(help = "Name or UUID of the password entry")] name: String, - #[structopt(help = "Username for the password entry")] + #[arg(help = "Username for the password entry")] user: Option<String>, - #[structopt(long, help = "Folder name to search in")] + #[arg(long, help = "Folder name to search in")] folder: Option<String>, }, - #[structopt(about = "Remove a given entry", visible_alias = "rm")] + #[command(about = "Remove a given entry", visible_alias = "rm")] Remove { - #[structopt(help = "Name or UUID of the password entry")] + #[arg(help = "Name or UUID of the password entry")] name: String, - #[structopt(help = "Username for the password entry")] + #[arg(help = "Username for the password entry")] user: Option<String>, - #[structopt(long, help = "Folder name to search in")] + #[arg(long, help = "Folder name to search in")] folder: Option<String>, }, - #[structopt(about = "View the password history for a given entry")] + #[command(about = "View the password history for a given entry")] History { - #[structopt(help = "Name or UUID of the password entry")] + #[arg(help = "Name or UUID of the password entry")] name: String, - #[structopt(help = "Username for the password entry")] + #[arg(help = "Username for the password entry")] user: Option<String>, - #[structopt(long, help = "Folder name to search in")] + #[arg(long, help = "Folder name to search in")] folder: Option<String>, }, - #[structopt(about = "Lock the password database")] + #[command(about = "Lock the password database")] Lock, - #[structopt(about = "Remove the local copy of the password database")] + #[command(about = "Remove the local copy of the password database")] Purge, - #[structopt( - name = "stop-agent", - about = "Terminate the background agent" - )] + #[command(name = "stop-agent", about = "Terminate the background agent")] StopAgent, - #[structopt( + + #[command( name = "gen-completions", about = "Generate completion script for the given shell" )] - GenCompletions { shell: String }, + GenCompletions { shell: clap_complete::Shell }, } impl Opt { @@ -246,20 +258,20 @@ impl Opt { } } -#[derive(Debug, structopt::StructOpt)] +#[derive(Debug, clap::Parser)] enum Config { - #[structopt(about = "Show the values of all configuration settings")] + #[command(about = "Show the values of all configuration settings")] Show, - #[structopt(about = "Set a configuration option")] + #[command(about = "Set a configuration option")] Set { - #[structopt(help = "Configuration key to set")] + #[arg(help = "Configuration key to set")] key: String, - #[structopt(help = "Value to set the configuration option to")] + #[arg(help = "Value to set the configuration option to")] value: String, }, - #[structopt(about = "Reset a configuration option to its default")] + #[command(about = "Reset a configuration option to its default")] Unset { - #[structopt(help = "Configuration key to unset")] + #[arg(help = "Configuration key to unset")] key: String, }, } @@ -275,15 +287,18 @@ impl Config { } } -#[paw::main] -fn main(opt: Opt) { +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((w, _)) = term_size::dimensions() { + if let Some((terminal_size::Width(w), _)) = + terminal_size::terminal_size() + { let out = format!("{}: {}", record.level(), record.args()); - writeln!(buf, "{}", textwrap::fill(&out, w - 1)) + writeln!(buf, "{}", textwrap::fill(&out, usize::from(w) - 1)) } else { writeln!(buf, "{}: {}", record.level(), record.args()) } @@ -306,8 +321,19 @@ fn main(opt: Opt) { name, user, folder, + field, full, - } => commands::get(name, user.as_deref(), folder.as_deref(), *full), + raw, + clipboard, + } => commands::get( + name, + user.as_deref(), + folder.as_deref(), + field.as_deref(), + *full, + *raw, + *clipboard, + ), Opt::Code { name, user, folder } => { commands::code(name, user.as_deref(), folder.as_deref()) } @@ -319,7 +345,7 @@ fn main(opt: Opt) { } => commands::add( name, user.as_deref(), - uri.iter() + &uri.iter() // XXX not sure what the ui for specifying the match type // should be .map(|uri| (uri.clone(), None)) @@ -351,7 +377,7 @@ fn main(opt: Opt) { commands::generate( name.as_deref(), user.as_deref(), - uri.iter() + &uri.iter() // XXX not sure what the ui for specifying the match type // should be .map(|uri| (uri.clone(), None)) @@ -373,25 +399,20 @@ fn main(opt: Opt) { Opt::Lock => commands::lock(), Opt::Purge => commands::purge(), Opt::StopAgent => commands::stop_agent(), - Opt::GenCompletions { shell } => gen_completions(shell), + 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); + eprintln!("{e:#}"); std::process::exit(1); } } - -fn gen_completions(shell: &str) -> anyhow::Result<()> { - let shell = match shell { - "bash" => structopt::clap::Shell::Bash, - "zsh" => structopt::clap::Shell::Zsh, - "fish" => structopt::clap::Shell::Fish, - "powershell" => structopt::clap::Shell::PowerShell, - "elvish" => structopt::clap::Shell::Elvish, - _ => return Err(anyhow::anyhow!("unknown shell {}", shell)), - }; - Opt::clap().gen_completions_to("rbw", shell, &mut std::io::stdout()); - Ok(()) -} diff --git a/src/cipherstring.rs b/src/cipherstring.rs index 39254c7..883cb34 100644 --- a/src/cipherstring.rs +++ b/src/cipherstring.rs @@ -1,10 +1,11 @@ use crate::prelude::*; -use block_modes::BlockMode as _; -use block_padding::Padding as _; -use hmac::{Mac as _, NewMac as _}; +use aes::cipher::{ + BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit as _, +}; +use hmac::Mac as _; +use pkcs8::DecodePrivateKey as _; use rand::RngCore as _; -use rsa::pkcs8::FromPrivateKey as _; use zeroize::Zeroize as _; pub enum CipherString { @@ -51,15 +52,15 @@ impl CipherString { }); } - let iv = base64::decode(parts[0]) + let iv = crate::base64::decode(parts[0]) .map_err(|source| Error::InvalidBase64 { source })?; - let ciphertext = base64::decode(parts[1]) + let ciphertext = crate::base64::decode(parts[1]) .map_err(|source| Error::InvalidBase64 { source })?; let mac = if parts.len() > 2 { - Some(base64::decode(parts[2]).map_err(|source| { - Error::InvalidBase64 { source } - })?) + Some(crate::base64::decode(parts[2]).map_err( + |source| Error::InvalidBase64 { source }, + )?) } else { None }; @@ -76,7 +77,7 @@ impl CipherString { // https://github.com/bitwarden/jslib/blob/785b681f61f81690de6df55159ab07ae710bcfad/src/enums/encryptionType.ts#L8 // format is: <cipher_text_b64>|<hmac_sig> let contents = contents.split('|').next().unwrap(); - let ciphertext = base64::decode(contents) + let ciphertext = crate::base64::decode(contents) .map_err(|source| Error::InvalidBase64 { source })?; Ok(Self::Asymmetric { ciphertext }) } @@ -98,12 +99,12 @@ impl CipherString { ) -> Result<Self> { let iv = random_iv(); - let cipher = block_modes::Cbc::< - aes::Aes256, - block_modes::block_padding::Pkcs7, - >::new_from_slices(keys.enc_key(), &iv) - .map_err(|source| Error::CreateBlockMode { source })?; - let ciphertext = cipher.encrypt_vec(plaintext); + let cipher = cbc::Encryptor::<aes::Aes256>::new( + keys.enc_key().into(), + iv.as_slice().into(), + ); + let ciphertext = + cipher.encrypt_padded_vec_mut::<block_padding::Pkcs7>(plaintext); let mut digest = hmac::Hmac::<sha2::Sha256>::new_from_slice(keys.mac_key()) @@ -136,7 +137,7 @@ impl CipherString { mac.as_deref(), )?; cipher - .decrypt_vec(ciphertext) + .decrypt_padded_vec_mut::<block_padding::Pkcs7>(ciphertext) .map_err(|source| Error::Decrypt { source }) } else { Err(Error::InvalidCipherString { @@ -166,7 +167,7 @@ impl CipherString { mac.as_deref(), )?; cipher - .decrypt(res.data_mut()) + .decrypt_padded_mut::<block_padding::Pkcs7>(res.data_mut()) .map_err(|source| Error::Decrypt { source })?; Ok(res) } else { @@ -184,15 +185,12 @@ impl CipherString { ) -> Result<crate::locked::Vec> { if let Self::Asymmetric { ciphertext } = self { let privkey_data = private_key.private_key(); - let privkey_data = block_padding::Pkcs7::unpad(privkey_data) - .map_err(|_| Error::Padding)?; + 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::padding::PaddingScheme::new_oaep::<sha1::Sha1>(), - ciphertext, - ) + .decrypt(rsa::Oaep::new::<sha1::Sha1>(), ciphertext) .map_err(|source| Error::Rsa { source })?; // XXX it'd be great if the rsa crate would let us decrypt @@ -218,8 +216,7 @@ fn decrypt_common_symmetric( iv: &[u8], ciphertext: &[u8], mac: Option<&[u8]>, -) -> Result<block_modes::Cbc<aes::Aes256, block_modes::block_padding::Pkcs7>> -{ +) -> Result<cbc::Decryptor<aes::Aes256>> { if let Some(mac) = mac { let mut key = hmac::Hmac::<sha2::Sha256>::new_from_slice(keys.mac_key()) @@ -227,15 +224,12 @@ fn decrypt_common_symmetric( key.update(iv); key.update(ciphertext); - if key.verify(mac).is_err() { + if key.verify(mac.into()).is_err() { return Err(Error::InvalidMac); } } - block_modes::Cbc::< - aes::Aes256, - block_modes::block_padding::Pkcs7, - >::new_from_slices(keys.enc_key(), iv) + cbc::Decryptor::<aes::Aes256>::new_from_slices(keys.enc_key(), iv) .map_err(|source| Error::CreateBlockMode { source }) } @@ -247,18 +241,18 @@ impl std::fmt::Display for CipherString { ciphertext, mac, } => { - let iv = base64::encode(&iv); - let ciphertext = base64::encode(&ciphertext); + let iv = crate::base64::encode(iv); + let ciphertext = crate::base64::encode(ciphertext); if let Some(mac) = &mac { - let mac = base64::encode(&mac); - write!(f, "2.{}|{}|{}", iv, ciphertext, mac) + let mac = crate::base64::encode(mac); + write!(f, "2.{iv}|{ciphertext}|{mac}") } else { - write!(f, "2.{}|{}", iv, ciphertext) + write!(f, "2.{iv}|{ciphertext}") } } Self::Asymmetric { ciphertext } => { - let ciphertext = base64::encode(&ciphertext); - write!(f, "4.{}", ciphertext) + let ciphertext = crate::base64::encode(ciphertext); + write!(f, "4.{ciphertext}") } } } @@ -270,3 +264,51 @@ fn random_iv() -> Vec<u8> { 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); + } +} diff --git a/src/config.rs b/src/config.rs index bbc39f7..efb1b5f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,51 +1,59 @@ use crate::prelude::*; use std::io::{Read as _, Write as _}; -use tokio::io::AsyncReadExt as _; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; #[derive(serde::Serialize, serde::Deserialize, Debug)] pub struct Config { pub email: Option<String>, pub base_url: Option<String>, pub identity_url: Option<String>, + pub notifications_url: Option<String>, #[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, - #[serde(default = "stub_device_id")] - pub device_id: String, + pub client_cert_path: Option<std::path::PathBuf>, + // backcompat, no longer generated in new configs + #[serde(skip_serializing)] + pub device_id: Option<String>, } impl Default for Config { fn default() -> Self { Self { - email: Default::default(), - base_url: Default::default(), - identity_url: Default::default(), + email: None, + base_url: None, + identity_url: None, + notifications_url: None, lock_timeout: default_lock_timeout(), + sync_interval: default_sync_interval(), pinentry: default_pinentry(), - device_id: default_device_id(), + client_cert_path: None, + device_id: None, } } } +#[must_use] pub fn default_lock_timeout() -> u64 { 3600 } -pub fn default_pinentry() -> String { - "pinentry".to_string() -} - -fn default_device_id() -> String { - uuid::Uuid::new_v4().to_hyphenated().to_string() +#[must_use] +pub fn default_sync_interval() -> u64 { + 3600 } -fn stub_device_id() -> String { - String::from("fix") +#[must_use] +pub fn default_pinentry() -> String { + "pinentry".to_string() } impl Config { + #[must_use] pub fn new() -> Self { Self::default() } @@ -127,36 +135,103 @@ impl Config { } pub fn validate() -> Result<()> { - let mut config = Self::load()?; + let config = Self::load()?; if config.email.is_none() { return Err(Error::ConfigMissingEmail); } - if config.device_id == stub_device_id() { - config.device_id = default_device_id(); - config.save()?; - } Ok(()) } + #[must_use] pub fn base_url(&self) -> String { self.base_url.clone().map_or_else( || "https://api.bitwarden.com".to_string(), - |url| format!("{}/api", url.trim_end_matches('/')), + |url| { + let clean_url = url.trim_end_matches('/').to_string(); + if clean_url == "https://api.bitwarden.eu" { + clean_url + } else { + format!("{clean_url}/api") + } + }, ) } + #[must_use] 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| format!("{}/identity", url.trim_end_matches('/')), + |url| { + let clean_url = url.trim_end_matches('/').to_string(); + if clean_url == "https://identity.bitwarden.eu" { + clean_url + } else { + format!("{clean_url}/identity") + } + }, + ) + }) + } + + #[must_use] + 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") + } + }, ) }) } + #[must_use] + pub fn client_cert_path(&self) -> Option<&std::path::Path> { + self.client_cert_path.as_deref() + } + + #[must_use] pub fn server_name(&self) -> String { self.base_url .clone() .unwrap_or_else(|| "default".to_string()) } } + +pub async fn device_id(config: &Config) -> Result<String> { + 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) + } +} @@ -164,7 +164,10 @@ pub struct Db { pub access_token: Option<String>, pub refresh_token: Option<String>, + pub kdf: Option<crate::api::KdfType>, pub iterations: Option<u32>, + pub memory: Option<u32>, + pub parallelism: Option<u32>, pub protected_key: Option<String>, pub protected_private_key: Option<String>, pub protected_org_keys: std::collections::HashMap<String, String>, @@ -173,6 +176,7 @@ pub struct Db { } impl Db { + #[must_use] pub fn new() -> Self { Self::default() } @@ -287,10 +291,12 @@ impl Db { Ok(()) } + #[must_use] 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() } } diff --git a/src/dirs.rs b/src/dirs.rs index 285a0d5..e838e12 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -37,59 +37,89 @@ pub fn make_all() -> Result<()> { Ok(()) } +#[must_use] 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':'); +#[must_use] 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!("{}:{}.json", server, email)) + cache_dir().join(format!("{server}:{email}.json")) } +#[must_use] pub fn pid_file() -> std::path::PathBuf { runtime_dir().join("pidfile") } +#[must_use] pub fn agent_stdout_file() -> std::path::PathBuf { data_dir().join("agent.out") } +#[must_use] pub fn agent_stderr_file() -> std::path::PathBuf { data_dir().join("agent.err") } +#[must_use] +pub fn device_id_file() -> std::path::PathBuf { + data_dir().join("device_id") +} + +#[must_use] pub fn socket_file() -> std::path::PathBuf { runtime_dir().join("socket") } +#[must_use] fn config_dir() -> std::path::PathBuf { - let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap(); + let project_dirs = + directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.config_dir().to_path_buf() } +#[must_use] fn cache_dir() -> std::path::PathBuf { - let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap(); + let project_dirs = + directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.cache_dir().to_path_buf() } +#[must_use] fn data_dir() -> std::path::PathBuf { - let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap(); + let project_dirs = + directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.data_dir().to_path_buf() } +#[must_use] fn runtime_dir() -> std::path::PathBuf { - let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap(); - match project_dirs.runtime_dir() { - Some(dir) => dir.to_path_buf(), - None => format!( - "{}/rbw-{}", - std::env::temp_dir().to_string_lossy(), - nix::unistd::getuid().as_raw() - ) - .into(), + let project_dirs = + directories::ProjectDirs::from("", "", &profile()).unwrap(); + project_dirs.runtime_dir().map_or_else( + || { + format!( + "{}/{}-{}", + std::env::temp_dir().to_string_lossy(), + &profile(), + nix::unistd::getuid().as_raw() + ) + .into() + }, + std::path::Path::to_path_buf, + ) +} + +#[must_use] +pub fn profile() -> String { + match std::env::var("RBW_PROFILE") { + Ok(profile) if !profile.is_empty() => format!("rbw-{profile}"), + _ => "rbw".to_string(), } } diff --git a/src/edit.rs b/src/edit.rs index 1a831a7..360f31f 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -2,7 +2,17 @@ use crate::prelude::*; use std::io::{Read as _, Write as _}; +use is_terminal::IsTerminal as _; + pub fn edit(contents: &str, help: &str) -> Result<String> { + 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"; @@ -30,6 +40,7 @@ pub fn edit(contents: &str, help: &str) -> Result<String> { 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") => { @@ -52,7 +63,7 @@ pub fn edit(contents: &str, help: &str) -> Result<String> { (editor, editor_args) }; - let res = std::process::Command::new(&cmd).args(&args).status(); + let res = std::process::Command::new(cmd).args(&args).status(); match res { Ok(res) => { if !res.success() { @@ -80,8 +91,6 @@ pub fn edit(contents: &str, help: &str) -> Result<String> { } fn contains_shell_metacharacters(cmd: &std::ffi::OsStr) -> bool { - match cmd.to_str() { - Some(s) => s.contains(&[' ', '$', '\'', '"'][..]), - None => false, - } + cmd.to_str() + .map_or(false, |s| s.contains(&[' ', '$', '\'', '"'][..])) } diff --git a/src/error.rs b/src/error.rs index 8116de2..8a3b5e2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,14 +4,10 @@ pub enum Error { ConfigMissingEmail, #[error("failed to create block mode decryptor")] - CreateBlockMode { - source: block_modes::InvalidKeyIvLength, - }, + CreateBlockMode { source: aes::cipher::InvalidLength }, #[error("failed to create block mode decryptor")] - CreateHmac { - source: hmac::crypto_mac::InvalidKeyLength, - }, + CreateHmac { source: aes::cipher::InvalidLength }, #[error("failed to create directory at {}", .file.display())] CreateDirectory { @@ -20,11 +16,14 @@ pub enum Error { }, #[error("failed to decrypt")] - Decrypt { source: block_modes::BlockModeError }, + Decrypt { source: block_padding::UnpadError }, #[error("failed to parse pinentry output ({out:?})")] FailedToParsePinentry { out: String }, + #[error("failed to read from stdin: {err}")] + FailedToReadFromStdin { err: std::io::Error }, + #[error( "failed to run editor {}: {err}", .editor.to_string_lossy(), @@ -116,6 +115,24 @@ pub enum 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("failed to load client cert from {}", .file.display())] + LoadClientCertReqwest { + source: reqwest::Error, + file: std::path::PathBuf, + }, + #[error("invalid padding")] Padding, @@ -125,6 +142,12 @@ pub enum Error { #[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, @@ -207,6 +230,9 @@ pub enum Error { #[error("error writing to pinentry stdin")] WriteStdin { source: tokio::io::Error }, + + #[error("invalid kdf type: {ty}")] + InvalidKdfType { ty: String }, } pub type Result<T> = std::result::Result<T, Error>; diff --git a/src/identity.rs b/src/identity.rs index 90d4fad..7836a11 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -1,5 +1,7 @@ use crate::prelude::*; +use sha1::Digest as _; + pub struct Identity { pub email: String, pub keys: crate::locked::Keys, @@ -10,7 +12,10 @@ impl Identity { pub fn new( email: &str, password: &crate::locked::Password, + kdf: crate::api::KdfType, iterations: u32, + memory: Option<u32>, + parallelism: Option<u32>, ) -> Result<Self> { let iterations = std::num::NonZeroU32::new(iterations) .ok_or(Error::Pbkdf2ZeroIterations)?; @@ -19,12 +24,43 @@ impl Identity { keys.extend(std::iter::repeat(0).take(64)); let enc_key = &mut keys.data_mut()[0..32]; - pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha256>>( - password.password(), - email.as_bytes(), - iterations.get(), - enc_key, - ); + + match kdf { + crate::api::KdfType::Pbkdf2 => { + pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha256>>( + 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)); @@ -33,7 +69,8 @@ impl Identity { password.password(), 1, hash.data_mut(), - ); + ) + .map_err(|_| Error::Pbkdf2)?; let hkdf = hkdf::Hkdf::<sha2::Sha256>::from_prk(enc_key) .map_err(|_| Error::HkdfExpand)?; @@ -1,21 +1,24 @@ +#![warn(clippy::cargo)] #![warn(clippy::pedantic)] #![warn(clippy::nursery)] -#![allow(clippy::default_trait_access)] -#![allow(clippy::implicit_hasher)] -#![allow(clippy::large_enum_variant)] +#![warn(clippy::as_conversions)] +#![warn(clippy::get_unwrap)] +#![allow(clippy::cognitive_complexity)] #![allow(clippy::missing_const_for_fn)] -#![allow(clippy::missing_errors_doc)] -#![allow(clippy::missing_panics_doc)] -#![allow(clippy::must_use_candidate)] #![allow(clippy::similar_names)] -#![allow(clippy::single_match)] +#![allow(clippy::struct_excessive_bools)] #![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_lines)] #![allow(clippy::type_complexity)] -#![allow(clippy::unused_async)] +#![allow(clippy::multiple_crate_versions)] +#![allow(clippy::large_enum_variant)] +// we aren't really documenting apis anyway +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::missing_panics_doc)] pub mod actions; pub mod api; +pub mod base64; pub mod cipherstring; pub mod config; pub mod db; diff --git a/src/locked.rs b/src/locked.rs index 4ddf021..bfa642d 100644 --- a/src/locked.rs +++ b/src/locked.rs @@ -18,10 +18,12 @@ impl Default for Vec { } impl Vec { + #[must_use] pub fn new() -> Self { Self::default() } + #[must_use] pub fn data(&self) -> &[u8] { self.data.as_slice() } @@ -65,10 +67,12 @@ pub struct Password { } impl Password { + #[must_use] pub fn new(password: Vec) -> Self { Self { password } } + #[must_use] pub fn password(&self) -> &[u8] { self.password.data() } @@ -80,14 +84,17 @@ pub struct Keys { } impl Keys { + #[must_use] pub fn new(keys: Vec) -> Self { Self { keys } } + #[must_use] pub fn enc_key(&self) -> &[u8] { &self.keys.data()[0..32] } + #[must_use] pub fn mac_key(&self) -> &[u8] { &self.keys.data()[32..64] } @@ -99,10 +106,12 @@ pub struct PasswordHash { } impl PasswordHash { + #[must_use] pub fn new(hash: Vec) -> Self { Self { hash } } + #[must_use] pub fn hash(&self) -> &[u8] { self.hash.data() } @@ -114,10 +123,12 @@ pub struct PrivateKey { } impl PrivateKey { + #[must_use] pub fn new(private_key: Vec) -> Self { Self { private_key } } + #[must_use] pub fn private_key(&self) -> &[u8] { self.private_key.data() } @@ -130,6 +141,7 @@ pub struct ApiKey { } impl ApiKey { + #[must_use] pub fn new(client_id: Password, client_secret: Password) -> Self { Self { client_id, @@ -137,10 +149,12 @@ impl ApiKey { } } + #[must_use] pub fn client_id(&self) -> &[u8] { self.client_id.password() } + #[must_use] pub fn client_secret(&self) -> &[u8] { self.client_secret.password() } diff --git a/src/pinentry.rs b/src/pinentry.rs index b4d2bb0..e2a83ed 100644 --- a/src/pinentry.rs +++ b/src/pinentry.rs @@ -1,5 +1,6 @@ use crate::prelude::*; +use std::convert::TryFrom as _; use tokio::io::AsyncWriteExt as _; pub async fn getpin( @@ -33,18 +34,18 @@ pub async fn getpin( .map_err(|source| Error::WriteStdin { source })?; ncommands += 1; stdin - .write_all(format!("SETPROMPT {}\n", prompt).as_bytes()) + .write_all(format!("SETPROMPT {prompt}\n").as_bytes()) .await .map_err(|source| Error::WriteStdin { source })?; ncommands += 1; stdin - .write_all(format!("SETDESC {}\n", desc).as_bytes()) + .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 {}\n", err).as_bytes()) + .write_all(format!("SETERROR {err}\n").as_bytes()) .await .map_err(|source| Error::WriteStdin { source })?; ncommands += 1; @@ -76,15 +77,13 @@ pub async fn getpin( Ok(crate::locked::Password::new(buf)) } -async fn read_password< - R: tokio::io::AsyncRead + tokio::io::AsyncReadExt + Unpin, ->( +async fn read_password<R>( mut ncommands: u8, data: &mut [u8], mut r: R, ) -> Result<usize> where - R: Send, + R: tokio::io::AsyncRead + tokio::io::AsyncReadExt + Unpin + Send, { let mut len = 0; loop { @@ -119,7 +118,7 @@ where }); } return Err(Error::PinentryErrorMessage { - error: format!("unknown error ({})", code), + error: format!("unknown error ({code})"), }); } None => { @@ -138,6 +137,14 @@ where .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; } } @@ -161,9 +168,11 @@ fn percent_decode(buf: &mut [u8]) -> usize { if c == b'%' && read_idx + 2 < len { if let Some(h) = char::from(buf[read_idx + 1]).to_digit(16) { - #[allow(clippy::cast_possible_truncation)] if let Some(l) = char::from(buf[read_idx + 2]).to_digit(16) { - c = h as u8 * 0x10 + l as u8; + // 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; } } diff --git a/src/protocol.rs b/src/protocol.rs index 14fa7f9..e883441 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,8 +1,6 @@ -// https://github.com/rust-lang/rust-clippy/issues/6902 -#![allow(clippy::use_self)] - // 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 +#[must_use] pub fn version() -> u32 { let major = env!("CARGO_PKG_VERSION_MAJOR"); let minor = env!("CARGO_PKG_VERSION_MINOR"); @@ -36,6 +34,9 @@ pub enum Action { plaintext: String, org_id: Option<String>, }, + ClipboardStore { + text: String, + }, Quit, Version, } diff --git a/src/pwgen.rs b/src/pwgen.rs index 55151e6..a112d73 100644 --- a/src/pwgen.rs +++ b/src/pwgen.rs @@ -15,6 +15,7 @@ pub enum Type { Diceware, } +#[must_use] pub fn pwgen(ty: Type, len: usize) -> String { let mut rng = rand::thread_rng(); @@ -101,6 +102,6 @@ mod test { for c in s.chars() { set.insert(c); } - assert!(set.len() < s.len()) + assert!(set.len() < s.len()); } } |