diff options
33 files changed, 4509 insertions, 1373 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 a51e8d6..730b8e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,119 @@ # Changelog +## [1.10.0] - 2024-04-20 + +### Added + +* `rbw get` now supports searching by URL as well (proxict, #132) +* `rbw code` now supports `--clipboard`, and has an alias of `rbw totp` (#127) + +### Changed + +* Set a user agent for all API calls, not just logging in (#165) + +### Fixed + +* Also create runtime directories when running with `--no-daemonize` (Wim de With, #155) +* Fix builds on NetBSD (#105) +* Fix logging in when the configured email address differs in case from the email address used when registering (#158) +* Fix editing passwords inadvertently clearing custom field values (#142) + +## [1.9.0] - 2024-01-01 + +### Added + +* Secure notes can now be edited (Tin Lai, #137) +* Piping passwords to `rbw edit` is now possible (Tin Lai, #138) + +### Fixed + +* More consistent behavior from `rbw get --field`, and fix some panics (Jörg Thalheim, #131) +* Fix handling of pinentry EOF (Jörg Thalheim, #140) +* Pass a user agent header to fix logging into the official bitwarden server (Maksim Karelov, #151) +* Support the official bitwarden.eu server (Edvin Åkerfeldt, #152) + +## [1.8.3] - 2023-07-20 + +### Fixed + +* Fixed running on linux without an X11 context available. (Benjamin Jacobs, + #126) + +## [1.8.2] - 2023-07-19 + +### Fixed + +* Fixed several issues with notification-based background syncing, it should + be much more reliable now. + +## [1.8.1] - 2023-07-18 + +### Fixed + +* `rbw config set notifications_url` now actually works + +## [1.8.0] - 2023-07-18 + +### Added + +* `rbw get --clipboard` to copy the result to the clipboard instead of + displaying it on stdout. (eatradish, #120) +* Background syncing now additionally happens when the server notifies the + agent of password updates, instead of needing to wait for the + `sync_interval` timer. (Bernd Schoolman, #115) +* New helper script `rbw-pinentry-keyring` which can be used as an alternate + pinentry program (via `rbw config set pinentry rbw-pinentry-keyring`) to + automatically read the master password from the system keyring. Currently + only supports the Gnome keyring via `secret-tool`. (Kai Frische, #122) +* Yubikeys in OTP mode are now supported for logging into a Bitwarden server. + (troyready, #123) + +### Fixed + +* Better error reporting when `rbw login` or `rbw register` fail. + +## [1.7.1] - 2023-03-27 + +### Fixed + +* argon2 actually works now (#113, Bernd Schoolmann) + +## [1.7.0] - 2023-03-25 + +### Added + +* `rbw` now automatically syncs the database from the server at a specified + interval while it is running. This defaults to once an hour, but is + configurable via the `sync_interval` option +* Email 2FA is now supported (#111, René 'Necoro' Neumann) +* argon2 KDF is now supported (#109, Bernd Schoolmann) + +### Fixed + +* `rbw --version` now works again + +## [1.6.0] - 2023-03-09 + +### Added + +* `rbw get` now supports a `--raw` option to display the entire contents of + the entry in JSON format (#97, classabbyamp) + +## [1.5.0] - 2023-02-18 + +### Added + +* Support for authenticating to self-hosted Bitwarden servers using client + certificates (#92, Filipe Pina) +* Support multiple independent profiles via the `RBW_PROFILE` environment + variable (#93, Skia) +* Add `rbw get --field` (#95, Jericho Keyne) + +### Fixed + +* Don't panic when not all stdout is read (#82, witcher) +* Fixed duplicated alias names in help output (#46) + ## [1.4.3] - 2022-02-10 ### Fixed @@ -3,83 +3,143 @@ 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.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", - "opaque-debug", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstream" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", ] [[package]] -name = "anyhow" -version = "1.0.55" +name = "anstyle" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] -name = "arrayvec" -version = "0.7.2" +name = "anstyle-parse" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] [[package]] -name = "async-trait" -version = "0.1.52" +name = "anstyle-query" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-sys 0.52.0", ] [[package]] -name = "atty" -version = "0.2.14" +name = "anstyle-wincon" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "anstyle", + "windows-sys 0.52.0", ] [[package]] -name = "autocfg" -version = "0.1.8" +name = "anyhow" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "autocfg 1.1.0", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] [[package]] name = "base32" @@ -89,15 +149,15 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base64" -version = "0.13.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[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" @@ -106,59 +166,102 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[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.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[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.9.1" +name = "bytes" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] -name = "byteorder" -version = "1.4.3" +name = "calloop" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.5.0", + "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 = "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 = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] [[package]] name = "cc" -version = "1.0.73" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" [[package]] name = "cfg-if" @@ -168,40 +271,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.34.0" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 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.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -209,121 +386,154 @@ 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.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] -name = "crypto-bigint" -version = "0.2.11" +name = "crossbeam-utils" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" -dependencies = [ - "generic-array", - "rand_core", - "subtle", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[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.5" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" 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 = "encoding_rs" -version = "0.8.30" +name = "dlib" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "cfg-if", + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", ] [[package]] name = "env_logger" -version = "0.9.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ - "atty", + "anstream", + "anstyle", + "env_filter", "humantime", "log", - "regex", - "termcolor", ] [[package]] -name = "fastrand" -version = "1.7.0" +name = "errno" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "instant", + "libc", + "windows-sys 0.52.0", ] [[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -331,55 +541,95 @@ 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.21" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -389,93 +639,76 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] -name = "getrandom" -version = "0.2.5" +name = "gethostname" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ - "cfg-if", "libc", - "wasi", + "windows-targets 0.48.5", ] [[package]] -name = "h2" -version = "0.3.11" +name = "getrandom" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "cfg-if", + "libc", + "wasi", ] [[package]] -name = "hashbrown" -version = "0.11.2" +name = "gimli" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "heck" -version = "0.3.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[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.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -484,26 +717,32 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", "http", - "pin-project-lite", ] [[package]] -name = "httparse" -version = "1.6.0" +name = "http-body-util" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] [[package]] -name = "httpdate" -version = "1.0.2" +name = "httparse" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "humantime" @@ -513,218 +752,246 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.17" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", - "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ + "futures-util", "http", "hyper", + "hyper-util", "rustls", + "rustls-pki-types", "tokio", "tokio-rustls", + "tower-service", ] [[package]] -name = "idna" -version = "0.2.3" +name = "hyper-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] -name = "indexmap" -version = "1.8.0" +name = "idna" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "autocfg 1.1.0", - "hashbrown", + "unicode-bidi", + "unicode-normalization", ] [[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 = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.56" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 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.119" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] [[package]] name = "libm" -version = "0.2.2" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] -name = "lock_api" -version = "0.4.6" +name = "libredox" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "scopeguard", + "bitflags 2.5.0", + "libc", ] [[package]] -name = "log" -version = "0.4.14" +name = "linux-raw-sys" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] -name = "mach" -version = "0.3.2" +name = "lock_api" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ - "libc", + "autocfg", + "scopeguard", ] [[package]] -name = "matches" -version = "0.1.9" +name = "log" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] -name = "memchr" -version = "2.4.1" +name = "mach2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] [[package]] -name = "memoffset" -version = "0.6.5" +name = "malloc_buf" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ - "autocfg 1.1.0", + "libc", ] [[package]] -name = "mime" -version = "0.3.16" +name = "memchr" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] -name = "mio" -version = "0.8.0" +name = "memmap2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", - "log", - "miow", - "ntapi", - "winapi", ] [[package]] -name = "miow" -version = "0.3.7" +name = "mime" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "nix" -version = "0.23.1" +name = "miniz_oxide" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", - "memoffset", + "adler", ] [[package]] -name = "ntapi" -version = "0.3.7" +name = "mio" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ - "winapi", + "libc", + "wasi", + "windows-sys 0.48.0", ] [[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.8", "byteorder", "lazy_static", "libm", @@ -738,56 +1005,87 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg 1.1.0", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.42" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ - "autocfg 1.1.0", + "autocfg", "libm", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] -name = "once_cell" -version = "1.9.0" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +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 = "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 = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl-probe" @@ -797,9 +1095,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", @@ -807,22 +1105,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.1" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "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", @@ -830,64 +1128,61 @@ dependencies = [ ] [[package]] -name = "paw" -version = "1.0.0" +name = "paste" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c0fc9b564dbc3dc2ed7c92c0c144f4de340aa94514ce2b446065417c4084e9" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "paw-attributes", - "paw-raw", + "digest", + "hmac", ] [[package]] -name = "paw-attributes" -version = "1.0.2" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f35583365be5d148e959284f42526841917b7bfa09e2d1a7ad5dde2cf0eaa39" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "proc-macro2", - "quote", - "syn", + "base64ct", ] [[package]] -name = "paw-raw" -version = "1.0.0" +name = "percent-encoding" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f0b59668fe80c5afe998f0c0bf93322bf2cd66cafeeb80581f291716f3467f2" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "pbkdf2" -version = "0.9.0" +name = "pin-project" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ - "crypto-mac", - "hmac", - "password-hash", - "sha2", + "pin-project-internal", ] [[package]] -name = "pem-rfc7468" -version = "0.2.4" +name = "pin-project-internal" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e93a3b1cc0510b03020f33f21e62acdde3dcaef432edc95bea377fbd4c2cd4" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "base64ct", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -897,72 +1192,75 @@ 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.16" +name = "pkg-config" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "polling" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "unicode-ident", ] [[package]] -name = "proc-macro2" -version = "1.0.36" +name = "quick-xml" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ - "unicode-xid", + "memchr", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -990,53 +1288,64 @@ 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 = "rbw" -version = "1.4.3" +version = "1.10.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", + "regex", "region", "reqwest", + "rmpv", "rsa", + "rustix", "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", @@ -1045,92 +1354,100 @@ 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.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", - "redox_syscall", + "libredox", + "thiserror", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", + "regex-automata", "regex-syntax", ] [[package]] -name = "regex-syntax" -version = "0.6.25" +name = "regex-automata" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] -name = "region" -version = "3.0.0" +name = "regex-syntax" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e189c2369884dce920945e2ddf79b3dff49e071a167dd1817fa9c4c00d512e" -dependencies = [ - "bitflags", - "libc", - "mach", - "winapi", -] +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "region" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" dependencies = [ - "winapi", + "bitflags 1.3.2", + "libc", + "mach2", + "windows-sys 0.52.0", ] [[package]] name = "reqwest" -version = "0.11.9" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ "base64", "bytes", - "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-rustls", + "hyper-util", "ipnet", "js-sys", - "lazy_static", "log", "mime", + "once_cell", "percent-encoding", "pin-project-lite", "rustls", "rustls-native-certs", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "tokio", "tokio-rustls", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1140,111 +1457,169 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", + "getrandom", "libc", - "once_cell", - "spin", + "spin 0.9.8", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", ] [[package]] -name = "rsa" -version = "0.5.0" +name = "rmp" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmpv" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e540282f11751956c82bc5529a7fb71b871b998fbf9cf06c2419b22e1b4350df" +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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" +dependencies = [ + "bitflags 2.5.0", + "errno", + "itoa", + "libc", + "linux-raw-sys", + "once_cell", + "windows-sys 0.52.0", +] + +[[package]] name = "rustls" -version = "0.20.4" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", - "sct", - "webpki", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", "rustls-pemfile", + "rustls-pki-types", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" -version = "0.2.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ "base64", + "rustls-pki-types", ] [[package]] -name = "ryu" -version = "1.0.9" +name = "rustls-pki-types" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" [[package]] -name = "schannel" -version = "0.1.19" +name = "rustls-webpki" +version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ - "lazy_static", - "winapi", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "scopeguard" -version = "1.1.0" +name = "ryu" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] -name = "sct" -version = "0.7.0" +name = "schannel" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "ring", - "untrusted", + "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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] name = "security-framework" -version = "2.6.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -1253,9 +1628,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -1263,18 +1638,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.136" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", @@ -1283,9 +1658,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -1294,18 +1669,19 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.7" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7868ad3b8196a8a0aea99a8220b124278ee5320a55e4fde97794b6f85b1a377" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ + "itoa", "serde", ] [[package]] name = "serde_repr" -version = "0.1.7" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", @@ -1325,194 +1701,211 @@ 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.9" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +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.8.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] -name = "socket2" -version = "0.4.4" +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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" dependencies = [ + "bitflags 2.5.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", "libc", - "winapi", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", ] [[package]] -name = "spin" -version = "0.5.2" +name = "smithay-clipboard" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] [[package]] -name = "spki" -version = "0.4.1" +name = "socket2" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ - "der", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "strsim" -version = "0.8.0" +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] -name = "structopt" -version = "0.3.26" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "paw", - "structopt-derive", -] +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "structopt-derive" -version = "0.4.18" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "base64ct", + "der", ] [[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "1.0.86" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "sync_wrapper" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "tempfile" -version = "3.3.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "term_size" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" -dependencies = [ - "libc", - "winapi", + "rustix", + "windows-sys 0.52.0", ] [[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ - "term_size", + "smawk", + "unicode-linebreak", "unicode-width", ] [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -1521,44 +1914,43 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +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.17.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ + "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.7.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -1567,146 +1959,207 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", + "rustls-pki-types", "tokio", - "webpki", ] [[package]] -name = "tokio-util" -version = "0.6.9" +name = "tokio-stream" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ - "bytes", "futures-core", - "futures-sink", - "log", "pin-project-lite", "tokio", ] [[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[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" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[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.31" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", + "log", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.22" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +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 = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", +] [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[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.9.0" +name = "unicode-linebreak" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] -name = "unicode-width" -version = "0.1.9" +name = "unicode-normalization" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +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.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" 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.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1714,25 +2167,24 @@ 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.79" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1740,13 +2192,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.79" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -1755,9 +2207,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.29" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -1767,9 +2219,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.79" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1777,9 +2229,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.79" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -1790,28 +2242,114 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.79" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] -name = "web-sys" -version = "0.3.56" +name = "wayland-backend" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" dependencies = [ - "js-sys", - "wasm-bindgen", + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", ] [[package]] -name = "webpki" -version = "0.22.0" +name = "wayland-client" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ - "ring", - "untrusted", + "bitflags 2.5.0", + "rustix", + "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.5.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +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.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", ] [[package]] @@ -1831,89 +2369,267 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "winapi-util" -version = "0.1.5" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "winapi", + "windows-targets 0.42.2", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-sys" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] [[package]] name = "windows-sys" -version = "0.32.0" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[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 = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[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.32.0" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[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 = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.32.0" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.32.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[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 = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.32.0" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[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 = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.32.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winreg" -version = "0.7.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] -name = "zeroize" -version = "1.4.3" +name = "x11-clipboard" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" dependencies = [ - "zeroize_derive", + "libc", + "x11rb", ] [[package]] -name = "zeroize_derive" -version = "1.3.2" +name = "x11rb" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", + "gethostname", + "rustix", + "x11rb-protocol", ] + +[[package]] +name = "x11rb-protocol" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" + +[[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.3" +version = "1.10.0" authors = ["Jesse Luehrs <doy@tozt.net>"] -edition = "2018" +edition = "2021" description = "Unofficial Bitwarden CLI" repository = "https://git.tozt.net/rbw" @@ -13,46 +13,59 @@ license = "MIT" include = ["src/**/*", "bin/**/*", "LICENSE", "README.md", "CHANGELOG.md"] [dependencies] -aes = "0.7.5" -anyhow = "1.0.53" -arrayvec = "0.7.2" -async-trait = "0.1.52" +aes = "0.8.4" +anyhow = "1.0.82" +argon2 = "0.5.3" +arrayvec = "0.7.4" +async-trait = "0.1.80" base32 = "0.4.0" -base64 = "0.13.0" -block-modes = "0.8.1" -block-padding = "0.2.1" -daemonize = "0.4.1" -directories = "4.0.1" -env_logger = "0.9.0" -hkdf = "0.11.0" -hmac = { version = "0.11.0", features = ["std"] } +base64 = "0.22.0" +block-padding = "0.3.3" +cbc = { version = "0.1.2", features = ["alloc", "std"] } +clap = { version = "4.5.4", features = ["wrap_help", "derive"] } +clap_complete = "4.5.2" +daemonize = "0.5.0" +# TODO: directories 5.0.1 uses MPL code, which isn't license-compatible +# we should switch to something else at some point +directories = "=5.0.0" +env_logger = "0.11.3" +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.117" -log = "0.4.14" -nix = "0.23.1" -paw = "1.0.0" -pbkdf2 = "0.9.0" -percent-encoding = "2.1.0" -rand = "0.8.4" -region = "3.0.0" -reqwest = { version = "0.11.9", default-features = false, features = ["blocking", "json", "rustls-tls-native-roots"] } -rsa = "0.5.0" -serde = { version = "1.0.136", features = ["derive"] } -serde_json = "1.0.78" -serde_path_to_error = "0.1.7" -serde_repr = "0.1.7" -sha-1 = "0.9.8" -sha2 = "0.9.9" -structopt = { version = "0.3.26", features = ["paw", "wrap_help"] } -tempfile = "3.3.0" -term_size = "0.3.2" -textwrap = "0.11.0" -thiserror = "1.0.30" -tokio = { version = "1.16.1", features = ["full"] } -totp-lite = "1.0.3" -url = "2.2.2" -uuid = { version = "0.8.2", features = ["v4"] } -zeroize = "1.4.3" +libc = "0.2.153" +log = "0.4.21" +pbkdf2 = "0.12.2" +percent-encoding = "2.3.1" +pkcs8 = "0.10.2" +rand = "0.8.5" +region = "3.0.2" +reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "rustls-tls-native-roots"] } +rsa = "0.9.6" +serde = { version = "1.0.198", features = ["derive"] } +serde_json = "1.0.116" +serde_path_to_error = "0.1.16" +serde_repr = "0.1.19" +sha1 = "0.10.6" +sha2 = "0.10.8" +tempfile = "3.10.1" +terminal_size = "0.3.0" +textwrap = "0.16.1" +thiserror = "1.0.58" +tokio = { version = "1.37.0", features = ["full"] } +tokio-stream = { version = "0.1.15", features = ["net"] } +totp-lite = "2.0.1" +url = "2.5.0" +uuid = { version = "1.8.0", features = ["v4"] } +zeroize = "1.7.0" +copypasta = "0.10.1" +rmpv = "1.0.2" +tokio-tungstenite = { version = "0.21", features = ["rustls-tls-native-roots"] } +is-terminal = "0.4.12" +regex = "1.10.4" +rustix = { version = "0.38.33", features = ["termios", "procfs", "process", "pipe"] } [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,10 +37,25 @@ You can download a Debian package from [`minisign`](https://github.com/jedisct1/minisign), and can be verified using the public key `RWTM0AZ5RpROOfAIWx1HvYQ6pw1+FKwN6526UFTKNImP/Hz3ynCFst3r`. +### Homebrew + +`rbw` is available in the [Homebrew repository](https://formulae.brew.sh/formula/rbw). You can install it via `brew install rbw`. + +### Nix + +`rbw` is available in the +[NixOS repository](https://search.nixos.org/packages?show=rbw). You can try +it out via `nix-shell -p rbw`. + +### Alpine + +`rbw` is available in the [testing repository](https://pkgs.alpinelinux.org/packages?name=rbw). +If you are not using the `edge` version of alpine you have to [enable the repository manually](https://wiki.alpinelinux.org/wiki/Repositories#Testing). + ### Other With a working Rust installation, `rbw` can be installed via `cargo install -rbw`. This requires that the +--locked rbw`. This requires that the [`pinentry`](https://www.gnupg.org/related_software/pinentry/index.en.html) program is installed (to display password prompts). @@ -56,13 +71,27 @@ 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`. +### Profiles + +`rbw` supports different configuration profiles, which can be switched +between by using the `RBW_PROFILE` environment variable. Setting it to a name +(for example, `RBW_PROFILE=work` or `RBW_PROFILE=personal`) can be used to +switch between several different vaults - each will use its own separate +configuration, local vault, and agent. + ## Usage Commands can generally be used directly, and will handle logging in or @@ -82,7 +111,11 @@ 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. In addition to matching against the name, +you can pass a UUID as the name to search for the entry with that id, or a +URL to search for an entry with a matching website entry. *Note to users of the official Bitwarden server (at bitwarden.com)*: The official server has a tendency to detect command line traffic as bot traffic @@ -95,3 +128,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-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,25 +1,49 @@ +[graph] targets = [ { triple = "x86_64-unknown-linux-musl" }, { triple = "x86_64-unknown-linux-gnu" }, + { triple = "x86_64-apple-darwin" }, + { triple = "aarch64-apple-darwin" }, ] [advisories] +version = 2 yanked = "deny" -unsound = "deny" +ignore = [ + # 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"] -copyleft = "deny" +version = 2 +allow = [ + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "Apache-2.0", + "ISC", + "Unicode-DFS-2016", +] exceptions = [ { name = "ring", allow = ["OpenSSL", "MIT", "ISC"] } ] @@ -31,3 +55,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 f6cef56..7ee1fa4 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -4,9 +4,7 @@ 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, &crate::config::device_id(&config).await?, &apikey) @@ -20,14 +18,27 @@ 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, @@ -38,13 +49,24 @@ pub async fn login( ) .await?; - Ok((access_token, refresh_token, iterations, protected_key)) + Ok(( + access_token, + refresh_token, + kdf, + iterations, + memory, + parallelism, + protected_key, + )) } 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, S>, @@ -52,8 +74,14 @@ pub fn unlock<S: std::hash::BuildHasher>( 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)?; @@ -121,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 } @@ -147,9 +173,7 @@ 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()); + let (client, _) = api_client()?; client.add(access_token, name, data, notes, folder_id)?; Ok(()) } @@ -161,6 +185,7 @@ pub fn edit( org_id: Option<&str>, name: &str, data: &crate::db::EntryData, + fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], @@ -172,6 +197,7 @@ pub fn edit( org_id, name, data, + fields, notes, folder_uuid, history, @@ -185,19 +211,19 @@ fn edit_once( org_id: Option<&str>, name: &str, data: &crate::db::EntryData, + fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<()> { - let 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, org_id, name, data, + fields, notes, folder_uuid, history, @@ -216,9 +242,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(()) } @@ -233,9 +257,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) } @@ -250,9 +272,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) } @@ -302,15 +322,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)) +} @@ -8,6 +8,8 @@ use crate::json::{ DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _, }; +use tokio::io::AsyncReadExt as _; + #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, @@ -39,7 +41,7 @@ impl std::fmt::Display for UriMatchType { RegularExpression => "regular_expression", Never => "never", }; - write!(f, "{}", s) + write!(f, "{s}") } } @@ -55,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 @@ -111,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}"), }), } } @@ -135,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, @@ -142,8 +261,14 @@ struct PreloginReq { #[derive(serde::Deserialize, Debug)] struct PreloginRes { + #[serde(rename = "Kdf", alias = "kdf")] + kdf: KdfType, #[serde(rename = "KdfIterations", alias = "kdfIterations")] kdf_iterations: u32, + #[serde(rename = "KdfMemory", alias = "kdfMemory")] + kdf_memory: Option<u32>, + #[serde(rename = "KdfParallelism", alias = "kdfParallelism")] + kdf_parallelism: Option<u32>, } #[derive(serde::Serialize, Debug)] @@ -237,7 +362,7 @@ struct SyncResCipher { #[serde(rename = "PasswordHistory", alias = "passwordHistory")] password_history: Option<Vec<SyncResPasswordHistory>>, #[serde(rename = "Fields", alias = "fields")] - fields: Option<Vec<SyncResField>>, + fields: Option<Vec<CipherField>>, #[serde(rename = "DeletedDate", alias = "deletedDate")] deleted_date: Option<String>, } @@ -338,8 +463,10 @@ impl SyncResCipher { fields .iter() .map(|field| crate::db::Field { + ty: field.ty, name: field.name.clone(), value: field.value.clone(), + linked_id: field.linked_id, }) .collect() }); @@ -457,6 +584,75 @@ struct CipherIdentity { username: Option<String>, } +#[derive( + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, + Debug, + Clone, + Copy, + PartialEq, + Eq, +)] +#[repr(u16)] +pub enum FieldType { + Text = 0, + Hidden = 1, + Boolean = 2, + Linked = 3, +} + +#[derive( + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, + Debug, + Clone, + Copy, + PartialEq, + Eq, +)] +#[repr(u16)] +pub enum LinkedIdType { + LoginUsername = 100, + LoginPassword = 101, + CardCardholderName = 300, + CardExpMonth = 301, + CardExpYear = 302, + CardCode = 303, + CardBrand = 304, + CardNumber = 305, + IdentityTitle = 400, + IdentityMiddleName = 401, + IdentityAddress1 = 402, + IdentityAddress2 = 403, + IdentityAddress3 = 404, + IdentityCity = 405, + IdentityState = 406, + IdentityPostalCode = 407, + IdentityCountry = 408, + IdentityCompany = 409, + IdentityEmail = 410, + IdentityPhone = 411, + IdentitySsn = 412, + IdentityUsername = 413, + IdentityPassportNumber = 414, + IdentityLicenseNumber = 415, + IdentityFirstName = 416, + IdentityLastName = 417, + IdentityFullName = 418, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +struct CipherField { + #[serde(rename = "Type", alias = "type")] + ty: FieldType, + #[serde(rename = "Name", alias = "name")] + name: Option<String>, + #[serde(rename = "Value", alias = "value")] + value: Option<String>, + #[serde(rename = "LinkedId", alias = "linkedId")] + linked_id: Option<LinkedIdType>, +} + // this is just a name and some notes, both of which are already on the cipher // object #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] @@ -470,16 +666,6 @@ struct SyncResPasswordHistory { password: Option<String>, } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -struct SyncResField { - #[serde(rename = "Type", alias = "type")] - ty: u32, - #[serde(rename = "Name", alias = "name")] - name: Option<String>, - #[serde(rename = "Value", alias = "value")] - value: Option<String>, -} - #[derive(serde::Serialize, Debug)] struct CiphersPostReq { #[serde(rename = "type")] @@ -508,6 +694,7 @@ struct CiphersPutReq { login: Option<CipherLogin>, card: Option<CipherCard>, identity: Option<CipherIdentity>, + fields: Vec<CipherField>, #[serde(rename = "secureNote")] secure_note: Option<CipherSecureNote>, #[serde(rename = "passwordHistory")] @@ -551,22 +738,70 @@ struct FoldersPostReq { pub struct Client { base_url: String, identity_url: String, + client_cert_path: Option<std::path::PathBuf>, } impl Client { #[must_use] - pub fn new(base_url: &str, identity_url: &str) -> Self { + 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), + } + } + + 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::CreateReqwestClient { source: e })?; + Ok(reqwest::Client::builder() + .user_agent(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .identity(pem) + .build() + .map_err(|e| Error::CreateReqwestClient { source: e })?) + } else { + Ok(reqwest::Client::builder() + .user_agent(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .build() + .map_err(|e| Error::CreateReqwestClient { source: e })?) } } - pub async fn prelogin(&self, email: &str) -> Result<u32> { + 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) @@ -574,7 +809,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( @@ -597,11 +837,11 @@ 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) @@ -612,7 +852,19 @@ impl Client { 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 }) + } + } } } @@ -627,28 +879,25 @@ 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), - // enum casts are safe, and i don't think there's a better way to - // write it without some explicit impls - #[allow(clippy::as_conversions)] 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), ) .send() .await @@ -663,7 +912,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 }) + } + } } } @@ -676,10 +937,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 })?; @@ -820,8 +1081,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 })?; @@ -843,12 +1104,18 @@ impl Client { org_id: Option<&str>, name: &str, data: &crate::db::EntryData, + fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<()> { let mut req = CiphersPutReq { - ty: 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(), @@ -857,6 +1124,15 @@ impl Client { card: None, identity: None, secure_note: None, + fields: fields + .iter() + .map(|field| CipherField { + ty: field.ty, + name: field.name.clone(), + value: field.value.clone(), + linked_id: field.linked_id, + }) + .collect(), password_history: history .iter() .map(|entry| CiphersPutReqHistory { @@ -953,8 +1229,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 })?; @@ -972,8 +1248,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() { @@ -993,8 +1269,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() { @@ -1025,8 +1301,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 })?; @@ -1055,7 +1331,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 })?; @@ -1072,7 +1348,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 87a8276..674442b 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, @@ -80,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()); @@ -89,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 @@ -101,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 @@ -112,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, @@ -125,52 +121,68 @@ 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; + 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")); } @@ -202,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 { @@ -214,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")?; @@ -232,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, )) } @@ -273,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, @@ -285,26 +318,29 @@ 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, @@ -312,7 +348,7 @@ async fn login_success( 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); } @@ -324,34 +360,35 @@ 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?; @@ -367,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, @@ -377,7 +417,10 @@ pub async fn unlock( match rbw::actions::unlock( &email, &password, + kdf, iterations, + memory, + parallelism, &protected_key, &protected_private_key, &db.protected_org_keys, @@ -407,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(()) @@ -419,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?; @@ -430,10 +473,9 @@ pub async fn lock( pub async fn check_lock( sock: &mut crate::sock::Sock, - state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, - _tty: Option<&str>, + state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>, ) -> anyhow::Result<()> { - if state.read().await.needs_unlock() { + if state.lock().await.needs_unlock() { return Err(anyhow::anyhow!("agent is locked")); } @@ -443,8 +485,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?; @@ -473,7 +515,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?; } @@ -482,14 +528,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" )); @@ -510,14 +554,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" )); @@ -533,6 +575,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(), @@ -607,3 +668,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 d64bf21..a3fecb4 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,99 +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 = None; - // no real better option to unwrap here - self.timeout_chan.send(TimeoutEvent::Clear).unwrap(); + 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: None, - timeout_chan: w, + 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 = self.timeout.as_mut().unwrap_or(&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 { @@ -144,12 +209,7 @@ async fn handle_request( true } rbw::protocol::Action::CheckLock => { - crate::actions::check_lock( - sock, - state.clone(), - req.tty.as_deref(), - ) - .await?; + crate::actions::check_lock(sock, state.clone()).await?; false } rbw::protocol::Action::Lock => { @@ -157,7 +217,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 { @@ -183,6 +243,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?; @@ -191,7 +256,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..06db891 100644 --- a/src/bin/rbw-agent/daemon.rs +++ b/src/bin/rbw-agent/daemon.rs @@ -1,25 +1,15 @@ pub struct StartupAck { - writer: std::os::unix::io::RawFd, + writer: std::os::unix::io::OwnedFd, } impl StartupAck { - pub fn ack(&self) -> anyhow::Result<()> { - nix::unistd::write(self.writer, &[0])?; - nix::unistd::close(self.writer)?; + pub fn ack(self) -> anyhow::Result<()> { + rustix::io::write(&self.writer, &[0])?; Ok(()) } } -impl Drop for StartupAck { - fn drop(&mut self) { - // best effort close here, can't do better in a destructor - let _ = nix::unistd::close(self.writer); - } -} - pub fn daemonize() -> anyhow::Result<StartupAck> { - rbw::dirs::make_all()?; - let stdout = std::fs::OpenOptions::new() .append(true) .create(true) @@ -29,33 +19,38 @@ pub fn daemonize() -> anyhow::Result<StartupAck> { .create(true) .open(rbw::dirs::agent_stderr_file())?; - let (r, w) = nix::unistd::pipe()?; - let res = daemonize::Daemonize::new() + let (r, w) = rustix::pipe::pipe()?; + 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(_) => { + drop(w); + let mut buf = [0; 1]; // 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(); - let _ = nix::unistd::close(r); + rustix::io::read(&r, &mut buf).unwrap(); + drop(r); + std::process::exit(0); + } + daemonize::Outcome::Child(res) => res, + }; + + drop(r); match res { Ok(_) => (), Err(e) => { - 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/debugger.rs b/src/bin/rbw-agent/debugger.rs index 59bbe50..be5260c 100644 --- a/src/bin/rbw-agent/debugger.rs +++ b/src/bin/rbw-agent/debugger.rs @@ -12,7 +12,7 @@ pub fn disable_tracing() -> anyhow::Result<()> { if ret == 0 { Ok(()) } else { - let e = nix::Error::last(); + let e = std::io::Error::last_os_error(); Err(anyhow::anyhow!("failed to disable PTRACE_ATTACH, agent memory may be dumpable by other processes: {}", e)) } } diff --git a/src/bin/rbw-agent/main.rs b/src/bin/rbw-agent/main.rs index f5d478d..d470e10 100644 --- a/src/bin/rbw-agent/main.rs +++ b/src/bin/rbw-agent/main.rs @@ -10,6 +10,10 @@ #![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 _; @@ -17,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>, @@ -28,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(()) @@ -44,6 +50,8 @@ fn real_main() -> anyhow::Result<()> { .nth(1) .map_or(false, |arg| arg == "--no-daemonize"); + rbw::dirs::make_all()?; + let startup_ack = if no_daemonize { None } else { @@ -78,7 +86,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 9458239..280b8cc 100644 --- a/src/bin/rbw-agent/sock.rs +++ b/src/bin/rbw-agent/sock.rs @@ -36,17 +36,14 @@ 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}"))) } } pub fn listen() -> anyhow::Result<tokio::net::UnixListener> { let path = rbw::dirs::socket_file(); // if the socket already doesn't exist, that's fine - // https://github.com/rust-lang/rust-clippy/issues/8003 - #[allow(clippy::let_underscore_drop)] let _ = std::fs::remove_file(&path); let sock = tokio::net::UnixListener::bind(&path) .context("failed to listen on socket")?; 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 321bff5..c84ccd4 100644 --- a/src/bin/rbw/actions.rs +++ b/src/bin/rbw/actions.rs @@ -1,4 +1,4 @@ -use anyhow::Context as _; +use anyhow::{bail, Context as _}; use std::io::Read as _; pub fn register() -> anyhow::Result<()> { @@ -31,11 +31,17 @@ 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 Some(pid) = + rustix::process::Pid::from_raw(pid.trim_end().parse()?) + else { + bail!("failed to read pid from pidfile"); + }; sock.send(&rbw::protocol::Request { - tty: nix::unistd::ttyname(0) + tty: rustix::termios::ttyname(std::io::stdin(), vec![]) .ok() - .and_then(|p| p.to_str().map(std::string::ToString::to_string)), + .and_then(|p| { + p.to_str().map(std::string::ToString::to_string).ok() + }), action: rbw::protocol::Action::Quit, })?; wait_for_exit(pid); @@ -57,9 +63,11 @@ pub fn decrypt( ) -> anyhow::Result<String> { let mut sock = connect()?; sock.send(&rbw::protocol::Request { - tty: nix::unistd::ttyname(0) + tty: rustix::termios::ttyname(std::io::stdin(), vec![]) .ok() - .and_then(|p| p.to_str().map(std::string::ToString::to_string)), + .and_then(|p| { + p.to_str().map(std::string::ToString::to_string).ok() + }), action: rbw::protocol::Action::Decrypt { cipherstring: cipherstring.to_string(), org_id: org_id.map(std::string::ToString::to_string), @@ -82,9 +90,11 @@ pub fn encrypt( ) -> anyhow::Result<String> { let mut sock = connect()?; sock.send(&rbw::protocol::Request { - tty: nix::unistd::ttyname(0) + tty: rustix::termios::ttyname(std::io::stdin(), vec![]) .ok() - .and_then(|p| p.to_str().map(std::string::ToString::to_string)), + .and_then(|p| { + p.to_str().map(std::string::ToString::to_string).ok() + }), action: rbw::protocol::Action::Encrypt { plaintext: plaintext.to_string(), org_id: org_id.map(std::string::ToString::to_string), @@ -101,12 +111,20 @@ 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) + tty: rustix::termios::ttyname(std::io::stdin(), vec![]) .ok() - .and_then(|p| p.to_str().map(std::string::ToString::to_string)), + .and_then(|p| { + p.to_str().map(std::string::ToString::to_string).ok() + }), action: rbw::protocol::Action::Version, })?; @@ -124,9 +142,11 @@ fn simple_action(action: rbw::protocol::Action) -> anyhow::Result<()> { let mut sock = connect()?; sock.send(&rbw::protocol::Request { - tty: nix::unistd::ttyname(0) + tty: rustix::termios::ttyname(std::io::stdin(), vec![]) .ok() - .and_then(|p| p.to_str().map(std::string::ToString::to_string)), + .and_then(|p| { + p.to_str().map(std::string::ToString::to_string).ok() + }), action, })?; @@ -152,9 +172,9 @@ fn connect() -> anyhow::Result<crate::sock::Sock> { }) } -fn wait_for_exit(pid: nix::unistd::Pid) { +fn wait_for_exit(pid: rustix::process::Pid) { loop { - if nix::sys::signal::kill(pid, None).is_err() { + if rustix::process::test_kill_process(pid).is_err() { break; } std::thread::sleep(std::time::Duration::from_millis(10)); diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 0068efd..3329f76 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -1,4 +1,9 @@ use anyhow::Context as _; +use serde::Serialize; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::io; +use std::io::prelude::Write; +use url::Url; const MISSING_CONFIG_HELP: &str = "Before using rbw, you must configure the email address you would like to \ @@ -11,6 +16,36 @@ const MISSING_CONFIG_HELP: &str = rbw config set identity_url <url>\n"; #[derive(Debug, Clone)] +pub enum Needle { + Name(String), + Uri(Url), + Uuid(uuid::Uuid), +} + +impl Display for Needle { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let value = match &self { + Self::Name(name) => name.clone(), + Self::Uri(uri) => uri.to_string(), + Self::Uuid(uuid) => uuid.to_string(), + }; + write!(f, "{value}") + } +} + +#[allow(clippy::unnecessary_wraps)] +pub fn parse_needle(arg: &str) -> Result<Needle, std::convert::Infallible> { + if let Ok(uuid) = uuid::Uuid::parse_str(arg) { + return Ok(Needle::Uuid(uuid)); + } + if let Ok(url) = Url::parse(arg) { + return Ok(Needle::Uri(url)); + } + + Ok(Needle::Name(arg.to_string())) +} + +#[derive(Debug, Clone, Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedCipher { id: String, @@ -23,30 +58,24 @@ 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, .. } => { password.as_ref().map_or_else( || { - eprintln!("entry for '{}' had no password", desc); + eprintln!("entry for '{desc}' had no password"); false }, - |password| { - println!("{}", password); - true - }, + |password| val_display_or_store(clipboard, password), ) } DecryptedData::Card { number, .. } => { number.as_ref().map_or_else( || { - eprintln!("entry for '{}' had no card number", desc); + eprintln!("entry for '{desc}' had no card number"); false }, - |number| { - println!("{}", number); - true - }, + |number| val_display_or_store(clipboard, number), ) } DecryptedData::Identity { @@ -60,31 +89,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 {} => self.notes.as_ref().map_or_else( || { - eprintln!("entry for '{}' had no notes", desc); + eprintln!("entry for '{desc}' had no notes"); false }, - |notes| { - println!("{}", notes); - true - }, + |notes| val_display_or_store(clipboard, notes), ), } } - fn display_long(&self, desc: &str) { + fn display_field(&self, desc: &str, field: &str, clipboard: bool) { + let field = field.to_lowercase(); + let field = field.as_str(); + match &self.data { + DecryptedData::Login { + username, + totp, + uris, + .. + } => match field { + "notes" => { + if let Some(notes) = &self.notes { + val_display_or_store(clipboard, notes); + } + } + "username" | "user" => { + if let Some(username) = &username { + val_display_or_store(clipboard, username); + } + } + "totp" | "code" => { + if let Some(totp) = totp { + match generate_totp(totp) { + Ok(code) => { + val_display_or_store(clipboard, &code); + } + Err(e) => { + eprintln!("{e}"); + } + } + } + } + "uris" | "urls" | "sites" => { + if let Some(uris) = uris { + let uri_strs: Vec<_> = uris + .iter() + .map(|uri| uri.uri.to_string()) + .collect(); + val_display_or_store(clipboard, &uri_strs.join("\n")); + } + } + "password" => { + self.display_short(desc, clipboard); + } + _ => { + for f in &self.fields { + if let Some(name) = &f.name { + if name.to_lowercase().as_str().contains(field) { + val_display_or_store( + clipboard, + f.value.as_deref().unwrap_or(""), + ); + break; + } + } + } + } + }, + DecryptedData::Card { + cardholder_name, + brand, + exp_month, + exp_year, + code, + .. + } => match field { + "number" | "card" => { + self.display_short(desc, clipboard); + } + "exp" => { + if let (Some(month), Some(year)) = (exp_month, exp_year) { + val_display_or_store( + clipboard, + &format!("{month}/{year}"), + ); + } + } + "exp_month" | "month" => { + if let Some(exp_month) = exp_month { + val_display_or_store(clipboard, exp_month); + } + } + "exp_year" | "year" => { + if let Some(exp_year) = exp_year { + val_display_or_store(clipboard, exp_year); + } + } + "cvv" => { + if let Some(code) = code { + val_display_or_store(clipboard, code); + } + } + "name" | "cardholder" => { + if let Some(cardholder_name) = cardholder_name { + val_display_or_store(clipboard, cardholder_name); + } + } + "brand" | "type" => { + if let Some(brand) = brand { + val_display_or_store(clipboard, brand); + } + } + "notes" => { + if let Some(notes) = &self.notes { + val_display_or_store(clipboard, notes); + } + } + _ => { + for f in &self.fields { + if let Some(name) = &f.name { + if name.to_lowercase().as_str().contains(field) { + val_display_or_store( + clipboard, + f.value.as_deref().unwrap_or(""), + ); + break; + } + } + } + } + }, + DecryptedData::Identity { + address1, + address2, + address3, + city, + state, + postal_code, + country, + phone, + email, + ssn, + license_number, + passport_number, + username, + .. + } => match field { + "name" => { + self.display_short(desc, clipboard); + } + "email" => { + if let Some(email) = email { + val_display_or_store(clipboard, email); + } + } + "address" => { + let mut strs = vec![]; + if let Some(address1) = address1 { + strs.push(address1.clone()); + } + if let Some(address2) = address2 { + strs.push(address2.clone()); + } + if let Some(address3) = address3 { + strs.push(address3.clone()); + } + if !strs.is_empty() { + val_display_or_store(clipboard, &strs.join("\n")); + } + } + "city" => { + if let Some(city) = city { + val_display_or_store(clipboard, city); + } + } + "state" => { + if let Some(state) = state { + val_display_or_store(clipboard, state); + } + } + "postcode" | "zipcode" | "zip" => { + if let Some(postal_code) = postal_code { + val_display_or_store(clipboard, postal_code); + } + } + "country" => { + if let Some(country) = country { + val_display_or_store(clipboard, country); + } + } + "phone" => { + if let Some(phone) = phone { + val_display_or_store(clipboard, phone); + } + } + "ssn" => { + if let Some(ssn) = ssn { + val_display_or_store(clipboard, ssn); + } + } + "license" => { + if let Some(license_number) = license_number { + val_display_or_store(clipboard, license_number); + } + } + "passport" => { + if let Some(passport_number) = passport_number { + val_display_or_store(clipboard, passport_number); + } + } + "username" => { + if let Some(username) = username { + val_display_or_store(clipboard, username); + } + } + "notes" => { + if let Some(notes) = &self.notes { + val_display_or_store(clipboard, notes); + } + } + _ => { + for f in &self.fields { + if let Some(name) = &f.name { + if name.to_lowercase().as_str().contains(field) { + val_display_or_store( + clipboard, + f.value.as_deref().unwrap_or(""), + ); + break; + } + } + } + } + }, + DecryptedData::SecureNote {} => match field { + "note" | "notes" => { + self.display_short(desc, clipboard); + } + _ => { + for f in &self.fields { + if let Some(name) = &f.name { + if name.to_lowercase().as_str().contains(field) { + val_display_or_store( + clipboard, + f.value.as_deref().unwrap_or(""), + ); + break; + } + } + } + } + }, + } + } + + fn display_long(&self, desc: &str, clipboard: bool) { match &self.data { DecryptedData::Login { username, @@ -92,18 +362,22 @@ impl DecryptedCipher { uris, .. } => { - let mut displayed = self.display_short(desc); - displayed |= display_field("Username", username.as_deref()); - displayed |= display_field("TOTP Secret", totp.as_deref()); + let mut displayed = self.display_short(desc, clipboard); + displayed |= + display_field("Username", username.as_deref(), clipboard); + displayed |= + display_field("TOTP Secret", totp.as_deref(), clipboard); if let Some(uris) = uris { for uri in uris { - displayed |= display_field("URI", Some(&uri.uri)); + displayed |= + display_field("URI", Some(&uri.uri), clipboard); let match_type = - uri.match_type.map(|ty| format!("{}", ty)); + uri.match_type.map(|ty| format!("{ty}")); displayed |= display_field( "Match type", match_type.as_deref(), + clipboard, ); } } @@ -112,6 +386,7 @@ impl DecryptedCipher { displayed |= display_field( field.name.as_deref().unwrap_or("(null)"), Some(field.value.as_deref().unwrap_or("")), + clipboard, ); } @@ -119,7 +394,7 @@ impl DecryptedCipher { if displayed { println!(); } - println!("{}", notes); + println!("{notes}"); } } DecryptedData::Card { @@ -130,24 +405,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 |= display_field("CVV", code.as_deref()); + displayed |= display_field("CVV", code.as_deref(), clipboard); + displayed |= display_field( + "Name", + cardholder_name.as_deref(), + clipboard, + ); displayed |= - display_field("Name", cardholder_name.as_deref()); - displayed |= 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 { @@ -166,34 +445,52 @@ impl DecryptedCipher { username, .. } => { - let mut displayed = self.display_short(desc); + let mut displayed = self.display_short(desc, clipboard); - displayed |= display_field("Address", address1.as_deref()); - displayed |= display_field("Address", address2.as_deref()); - displayed |= display_field("Address", address3.as_deref()); - displayed |= display_field("City", city.as_deref()); - displayed |= display_field("State", state.as_deref()); displayed |= - display_field("Postcode", postal_code.as_deref()); - displayed |= display_field("Country", country.as_deref()); - displayed |= display_field("Phone", phone.as_deref()); - displayed |= display_field("Email", email.as_deref()); - displayed |= display_field("SSN", ssn.as_deref()); + display_field("Address", address1.as_deref(), clipboard); + displayed |= + display_field("Address", address2.as_deref(), clipboard); + displayed |= + display_field("Address", address3.as_deref(), clipboard); displayed |= - display_field("License", license_number.as_deref()); + display_field("City", city.as_deref(), clipboard); displayed |= - display_field("Passport", passport_number.as_deref()); - displayed |= display_field("Username", username.as_deref()); + display_field("State", state.as_deref(), clipboard); + displayed |= display_field( + "Postcode", + postal_code.as_deref(), + clipboard, + ); + displayed |= + display_field("Country", country.as_deref(), clipboard); + displayed |= + display_field("Phone", phone.as_deref(), clipboard); + displayed |= + display_field("Email", email.as_deref(), clipboard); + displayed |= display_field("SSN", ssn.as_deref(), clipboard); + displayed |= display_field( + "License", + license_number.as_deref(), + clipboard, + ); + displayed |= display_field( + "Passport", + passport_number.as_deref(), + clipboard, + ); + displayed |= + display_field("Username", username.as_deref(), clipboard); if let Some(notes) = &self.notes { if displayed { println!(); } - println!("{}", notes); + println!("{notes}"); } } DecryptedData::SecureNote {} => { - self.display_short(desc); + self.display_short(desc, clipboard); } } } @@ -210,15 +507,48 @@ impl DecryptedCipher { } } + 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, + needle: &Needle, username: Option<&str>, folder: Option<&str>, try_match_folder: bool, ) -> bool { - if name != self.name { - return false; + match needle { + Needle::Name(name) => { + if &self.name != name { + return false; + } + } + Needle::Uri(given_uri) => { + match &self.data { + DecryptedData::Login { + uris: Some(uris), .. + } => { + if !uris.iter().any(|uri| uri.matches_url(given_uri)) + { + return false; + } + } + _ => { + // not sure what else to do here, but open to suggestions + return false; + } + } + } + Needle::Uuid(uuid) => { + if uuid::Uuid::parse_str(&self.id) != Ok(*uuid) { + return false; + } + } } if let Some(given_username) = username { @@ -301,9 +631,24 @@ 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))] -#[allow(clippy::large_enum_variant)] enum DecryptedData { Login { username: Option<String>, @@ -341,27 +686,97 @@ 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, match_type: Option<rbw::api::UriMatchType>, } +impl DecryptedUri { + fn matches_url(&self, url: &Url) -> bool { + match self.match_type.unwrap_or(rbw::api::UriMatchType::Domain) { + rbw::api::UriMatchType::Domain => { + let Some(given_domain_port) = domain_port(url) else { + return false; + }; + if let Ok(self_url) = url::Url::parse(&self.uri) { + if let Some(self_domain_port) = domain_port(&self_url) { + if self_url.scheme() == url.scheme() + && (self_domain_port == given_domain_port + || given_domain_port.ends_with(&format!( + ".{self_domain_port}" + ))) + { + return true; + } + } + } + self.uri == given_domain_port + || given_domain_port.ends_with(&format!(".{}", self.uri)) + } + rbw::api::UriMatchType::Host => { + let Some(given_host_port) = host_port(url) else { + return false; + }; + if let Ok(self_url) = url::Url::parse(&self.uri) { + if let Some(self_host_port) = host_port(&self_url) { + if self_url.scheme() == url.scheme() + && self_host_port == given_host_port + { + return true; + } + } + } + self.uri == given_host_port + } + rbw::api::UriMatchType::StartsWith => { + url.to_string().starts_with(&self.uri) + } + rbw::api::UriMatchType::Exact => url.to_string() == self.uri, + rbw::api::UriMatchType::RegularExpression => { + let Ok(rx) = regex::Regex::new(&self.uri) else { + return false; + }; + rx.is_match(url.as_ref()) + } + rbw::api::UriMatchType::Never => false, + } + } +} + +fn host_port(url: &Url) -> Option<String> { + let host = url.host_str()?; + Some( + url.port().map_or_else( + || host.to_string(), + |port| format!("{host}:{port}"), + ), + ) +} + +fn domain_port(url: &Url) -> Option<String> { + let domain = url.domain()?; + Some(url.port().map_or_else( + || domain.to_string(), + |port| format!("{domain}:{port}"), + )) +} + enum ListField { Name, Id, @@ -383,11 +798,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()?; @@ -405,6 +825,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() @@ -415,6 +842,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)), } @@ -437,6 +870,8 @@ 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(); } @@ -455,6 +890,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()?; @@ -504,8 +946,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)); @@ -518,29 +959,38 @@ pub fn list(fields: &[String]) -> anyhow::Result<()> { ListField::User => match &cipher.data { DecryptedData::Login { username, .. } => { username.as_ref().map_or_else( - || "".to_string(), + String::new, std::string::ToString::to_string, ) } - _ => "".to_string(), + _ => String::new(), }, ListField::Folder => cipher.folder.as_ref().map_or_else( - || "".to_string(), + 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(()) } pub fn get( - name: &str, + needle: &Needle, user: Option<&str>, folder: Option<&str>, + field: Option<&str>, full: bool, + raw: bool, + clipboard: bool, ) -> anyhow::Result<()> { unlock()?; @@ -548,16 +998,20 @@ pub fn get( let desc = format!( "{}{}", - user.map_or_else(|| "".to_string(), |s| format!("{}@", s)), - name + user.map_or_else(String::new, |s| format!("{s}@")), + needle ); - let (_, decrypted) = find_entry(&db, name, user, folder) - .with_context(|| format!("couldn't find entry for '{}'", desc))?; - if full { - decrypted.display_long(&desc); + let (_, decrypted) = find_entry(&db, needle, user, folder) + .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(()) @@ -567,6 +1021,7 @@ pub fn code( name: &str, user: Option<&str>, folder: Option<&str>, + clipboard: bool, ) -> anyhow::Result<()> { unlock()?; @@ -574,16 +1029,17 @@ pub fn code( let desc = format!( "{}{}", - user.map_or_else(|| "".to_string(), |s| format!("{}@", s)), + 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))?; + let (_, decrypted) = + find_entry(&db, &Needle::Name(name.to_string()), user, folder) + .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)?); + val_display_or_store(clipboard, &generate_totp(&totp)?); } else { return Err(anyhow::anyhow!( "entry does not contain a totp secret" @@ -616,7 +1072,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 @@ -702,7 +1158,7 @@ pub fn generate( ty: rbw::pwgen::Type, ) -> anyhow::Result<()> { let password = rbw::pwgen::pwgen(ty, len); - println!("{}", password); + println!("{password}"); if let Some(name) = name { unlock()?; @@ -802,22 +1258,23 @@ pub fn edit( let desc = format!( "{}{}", - username.map_or_else(|| "".to_string(), |s| format!("{}@", s)), + 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))?; + let (entry, decrypted) = + find_entry(&db, &Needle::Name(name.to_string()), username, folder) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; - let (data, notes, history) = match &decrypted.data { + let (data, fields, notes, history) = match &decrypted.data { DecryptedData::Login { password, .. } => { let mut contents = format!("{}\n", password.as_deref().unwrap_or("")); if let Some(notes) = decrypted.notes { - contents.push_str(&format!("\n{}\n", notes)); + contents.push_str(&format!("\n{notes}\n")); } - let contents = rbw::edit::edit(&contents, HELP)?; + let contents = rbw::edit::edit(&contents, HELP_PW)?; let (password, notes) = parse_editor(&contents); let password = password @@ -834,16 +1291,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 { @@ -864,11 +1320,31 @@ pub fn edit( uris: entry_uris.clone(), totp: entry_totp.clone(), }; - (data, notes, history) + (data, entry.fields, notes, history) + } + DecryptedData::SecureNote {} => { + let data = rbw::db::EntryData::SecureNote {}; + + let editor_content = decrypted.notes.map_or_else( + || "\n".to_string(), + |notes| format!("{notes}\n"), + ); + let contents = rbw::edit::edit(&editor_content, HELP_NOTES)?; + + // prepend blank line to be parsed as pw by `parse_editor` + let (_, notes) = parse_editor(&format!("\n{contents}\n")); + + let notes = notes + .map(|notes| { + crate::actions::encrypt(¬es, entry.org_id.as_deref()) + }) + .transpose()?; + + (data, entry.fields, notes, entry.history) } _ => { return Err(anyhow::anyhow!( - "modifications are only supported for login entries" + "modifications are only supported for login and note entries" )); } }; @@ -880,6 +1356,7 @@ pub fn edit( entry.org_id.as_deref(), &entry.name, &data, + &fields, notes.as_deref(), entry.folder_id.as_deref(), &history, @@ -905,12 +1382,13 @@ pub fn remove( let desc = format!( "{}{}", - username.map_or_else(|| "".to_string(), |s| format!("{}@", s)), + 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))?; + let (entry, _) = + find_entry(&db, &Needle::Name(name.to_string()), username, folder) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let (Some(access_token), ()) = rbw::actions::remove(access_token, refresh_token, &entry.id)? @@ -935,12 +1413,13 @@ pub fn history( let desc = format!( "{}{}", - username.map_or_else(|| "".to_string(), |s| format!("{}@", s)), + 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))?; + let (_, decrypted) = + find_entry(&db, &Needle::Name(name.to_string()), username, folder) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; for history in decrypted.history { println!("{}: {}", history.last_used_date, history.password); } @@ -1028,8 +1507,6 @@ fn check_config() -> anyhow::Result<()> { fn version_or_quit() -> anyhow::Result<u32> { crate::actions::version().map_err(|e| { - // https://github.com/rust-lang/rust-clippy/issues/8003 - #[allow(clippy::let_underscore_drop)] let _ = crate::actions::quit(); e }) @@ -1037,13 +1514,13 @@ fn version_or_quit() -> anyhow::Result<u32> { fn find_entry( db: &rbw::db::Db, - name: &str, + needle: &Needle, username: Option<&str>, folder: Option<&str>, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { - if uuid::Uuid::parse_str(name).is_ok() { + if let Needle::Uuid(uuid) = needle { for cipher in &db.entries { - if name == cipher.id { + if uuid::Uuid::parse_str(&cipher.id) == Ok(*uuid) { return Ok((cipher.clone(), decrypt_cipher(cipher)?)); } } @@ -1057,22 +1534,22 @@ fn find_entry( decrypt_cipher(&entry).map(|decrypted| (entry, decrypted)) }) .collect::<anyhow::Result<_>>()?; - find_entry_raw(&ciphers, name, username, folder) + find_entry_raw(&ciphers, needle, username, folder) } } fn find_entry_raw( entries: &[(rbw::db::Entry, DecryptedCipher)], - name: &str, + needle: &Needle, username: Option<&str>, folder: Option<&str>, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { let mut matches: Vec<(rbw::db::Entry, DecryptedCipher)> = entries .iter() - .cloned() - .filter(|(_, decrypted_cipher)| { - decrypted_cipher.exact_match(name, username, folder, true) + .filter(|&(_, decrypted_cipher)| { + decrypted_cipher.exact_match(needle, username, folder, true) }) + .cloned() .collect(); if matches.len() == 1 { @@ -1082,10 +1559,10 @@ fn find_entry_raw( if folder.is_none() { matches = entries .iter() - .cloned() - .filter(|(_, decrypted_cipher)| { - decrypted_cipher.exact_match(name, username, folder, false) + .filter(|&(_, decrypted_cipher)| { + decrypted_cipher.exact_match(needle, username, folder, false) }) + .cloned() .collect(); if matches.len() == 1 { @@ -1093,29 +1570,32 @@ fn find_entry_raw( } } - matches = entries - .iter() - .cloned() - .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, true) - }) - .collect(); - - if matches.len() == 1 { - return Ok(matches[0].clone()); - } - - if folder.is_none() { + if let Needle::Name(name) = needle { matches = entries .iter() - .cloned() - .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, false) + .filter(|&(_, decrypted_cipher)| { + decrypted_cipher.partial_match(name, username, folder, true) }) + .cloned() .collect(); + if matches.len() == 1 { return Ok(matches[0].clone()); } + + if folder.is_none() { + matches = entries + .iter() + .filter(|&(_, decrypted_cipher)| { + decrypted_cipher + .partial_match(name, username, folder, false) + }) + .cloned() + .collect(); + if matches.len() == 1 { + return Ok(matches[0].clone()); + } + } } if matches.is_empty() { @@ -1417,8 +1897,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(); } @@ -1485,7 +1968,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")) } @@ -1502,6 +1985,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::*; @@ -1509,15 +1999,15 @@ mod test { #[test] fn test_find_entry() { let entries = &[ - make_entry("github", Some("foo"), None), - make_entry("gitlab", Some("foo"), None), - make_entry("gitlab", Some("bar"), None), - make_entry("gitter", Some("baz"), None), - make_entry("git", Some("foo"), None), - make_entry("bitwarden", None, None), - make_entry("github", Some("foo"), Some("websites")), - make_entry("github", Some("foo"), Some("ssh")), - make_entry("github", Some("root"), Some("ssh")), + make_entry("github", Some("foo"), None, &[]), + make_entry("gitlab", Some("foo"), None, &[]), + make_entry("gitlab", Some("bar"), None, &[]), + make_entry("gitter", Some("baz"), None, &[]), + make_entry("git", Some("foo"), None, &[]), + make_entry("bitwarden", None, None, &[]), + make_entry("github", Some("foo"), Some("websites"), &[]), + make_entry("github", Some("foo"), Some("ssh"), &[]), + make_entry("github", Some("root"), Some("ssh"), &[]), ]; assert!( @@ -1576,47 +2066,681 @@ mod test { ); } + #[test] + fn test_find_by_uuid() { + let entries = &[ + make_entry("github", Some("foo"), None, &[]), + make_entry("gitlab", Some("foo"), None, &[]), + make_entry("gitlab", Some("bar"), None, &[]), + ]; + + assert!( + one_match(entries, &entries[0].0.id, None, None, 0), + "foo@github" + ); + assert!( + one_match(entries, &entries[1].0.id, None, None, 1), + "foo@gitlab" + ); + assert!( + one_match(entries, &entries[2].0.id, None, None, 2), + "bar@gitlab" + ); + + assert!( + one_match( + entries, + &entries[0].0.id.to_uppercase(), + None, + None, + 0 + ), + "foo@github" + ); + assert!( + one_match( + entries, + &entries[0].0.id.to_lowercase(), + None, + None, + 0 + ), + "foo@github" + ); + } + + #[test] + fn test_find_by_url_default() { + let entries = &[ + make_entry("one", None, None, &[("https://one.com/", None)]), + make_entry("two", None, None, &[("https://two.com/login", None)]), + make_entry( + "three", + None, + None, + &[("https://login.three.com/", None)], + ), + make_entry("four", None, None, &[("four.com", None)]), + make_entry( + "five", + None, + None, + &[("https://five.com:8080/", None)], + ), + make_entry("six", None, None, &[("six.com:8080", None)]), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + one_match(entries, "https://login.one.com/", None, None, 0), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!(one_match(entries, "https://two.com/", None, None, 1), "two"); + assert!( + one_match(entries, "https://two.com/other-page", None, None, 1), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + + assert!( + one_match(entries, "https://four.com/", None, None, 3), + "four" + ); + + assert!( + one_match(entries, "https://five.com:8080/", None, None, 4), + "five" + ); + assert!(no_matches(entries, "https://five.com/", None, None), "five"); + + assert!( + one_match(entries, "https://six.com:8080/", None, None, 5), + "six" + ); + assert!(no_matches(entries, "https://six.com/", None, None), "six"); + } + + #[test] + fn test_find_by_url_domain() { + let entries = &[ + make_entry( + "one", + None, + None, + &[("https://one.com/", Some(rbw::api::UriMatchType::Domain))], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::Domain), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::Domain), + )], + ), + make_entry( + "four", + None, + None, + &[("four.com", Some(rbw::api::UriMatchType::Domain))], + ), + make_entry( + "five", + None, + None, + &[( + "https://five.com:8080/", + Some(rbw::api::UriMatchType::Domain), + )], + ), + make_entry( + "six", + None, + None, + &[("six.com:8080", Some(rbw::api::UriMatchType::Domain))], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + one_match(entries, "https://login.one.com/", None, None, 0), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!(one_match(entries, "https://two.com/", None, None, 1), "two"); + assert!( + one_match(entries, "https://two.com/other-page", None, None, 1), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + + assert!( + one_match(entries, "https://four.com/", None, None, 3), + "four" + ); + + assert!( + one_match(entries, "https://five.com:8080/", None, None, 4), + "five" + ); + assert!(no_matches(entries, "https://five.com/", None, None), "five"); + + assert!( + one_match(entries, "https://six.com:8080/", None, None, 5), + "six" + ); + assert!(no_matches(entries, "https://six.com/", None, None), "six"); + } + + #[test] + fn test_find_by_url_host() { + let entries = &[ + make_entry( + "one", + None, + None, + &[("https://one.com/", Some(rbw::api::UriMatchType::Host))], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::Host), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::Host), + )], + ), + make_entry( + "four", + None, + None, + &[("four.com", Some(rbw::api::UriMatchType::Host))], + ), + make_entry( + "five", + None, + None, + &[( + "https://five.com:8080/", + Some(rbw::api::UriMatchType::Host), + )], + ), + make_entry( + "six", + None, + None, + &[("six.com:8080", Some(rbw::api::UriMatchType::Host))], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!(one_match(entries, "https://two.com/", None, None, 1), "two"); + assert!( + one_match(entries, "https://two.com/other-page", None, None, 1), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + + assert!( + one_match(entries, "https://four.com/", None, None, 3), + "four" + ); + + assert!( + one_match(entries, "https://five.com:8080/", None, None, 4), + "five" + ); + assert!(no_matches(entries, "https://five.com/", None, None), "five"); + + assert!( + one_match(entries, "https://six.com:8080/", None, None, 5), + "six" + ); + assert!(no_matches(entries, "https://six.com/", None, None), "six"); + } + + #[test] + fn test_find_by_url_starts_with() { + let entries = &[ + make_entry( + "one", + None, + None, + &[( + "https://one.com/", + Some(rbw::api::UriMatchType::StartsWith), + )], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::StartsWith), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::StartsWith), + )], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!( + one_match(entries, "https://two.com/login", None, None, 1), + "two" + ); + assert!( + one_match(entries, "https://two.com/login/sso", None, None, 1), + "two" + ); + assert!(no_matches(entries, "https://two.com/", None, None), "two"); + assert!( + no_matches(entries, "https://two.com/other-page", None, None), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + } + + #[test] + fn test_find_by_url_exact() { + let entries = &[ + make_entry( + "one", + None, + None, + &[("https://one.com/", Some(rbw::api::UriMatchType::Exact))], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::Exact), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::Exact), + )], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!( + one_match(entries, "https://two.com/login", None, None, 1), + "two" + ); + assert!( + no_matches(entries, "https://two.com/login/sso", None, None), + "two" + ); + assert!(no_matches(entries, "https://two.com/", None, None), "two"); + assert!( + no_matches(entries, "https://two.com/other-page", None, None), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + } + + #[test] + fn test_find_by_url_regex() { + let entries = &[ + make_entry( + "one", + None, + None, + &[( + r"^https://one\.com/$", + Some(rbw::api::UriMatchType::RegularExpression), + )], + ), + make_entry( + "two", + None, + None, + &[( + r"^https://two\.com/(login|start)", + Some(rbw::api::UriMatchType::RegularExpression), + )], + ), + make_entry( + "three", + None, + None, + &[( + r"^https://(login\.)?three\.com/$", + Some(rbw::api::UriMatchType::RegularExpression), + )], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!( + one_match(entries, "https://two.com/login", None, None, 1), + "two" + ); + assert!( + one_match(entries, "https://two.com/start", None, None, 1), + "two" + ); + assert!( + one_match(entries, "https://two.com/login/sso", None, None, 1), + "two" + ); + assert!(no_matches(entries, "https://two.com/", None, None), "two"); + assert!( + no_matches(entries, "https://two.com/other-page", None, None), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + one_match(entries, "https://three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://www.three.com/", None, None), + "three" + ); + } + + #[test] + fn test_find_by_url_never() { + let entries = &[ + make_entry( + "one", + None, + None, + &[("https://one.com/", Some(rbw::api::UriMatchType::Never))], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::Never), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::Never), + )], + ), + make_entry( + "four", + None, + None, + &[("four.com", Some(rbw::api::UriMatchType::Never))], + ), + make_entry( + "five", + None, + None, + &[( + "https://five.com:8080/", + Some(rbw::api::UriMatchType::Never), + )], + ), + make_entry( + "six", + None, + None, + &[("six.com:8080", Some(rbw::api::UriMatchType::Never))], + ), + ]; + + assert!(no_matches(entries, "https://one.com/", None, None), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + no_matches(entries, "https://one.com:443/", None, None), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!(no_matches(entries, "https://two.com/", None, None), "two"); + assert!( + no_matches(entries, "https://two.com/other-page", None, None), + "two" + ); + + assert!( + no_matches(entries, "https://login.three.com/", None, None), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + + assert!(no_matches(entries, "https://four.com/", None, None), "four"); + + assert!( + no_matches(entries, "https://five.com:8080/", None, None), + "five" + ); + assert!(no_matches(entries, "https://five.com/", None, None), "five"); + + assert!( + no_matches(entries, "https://six.com:8080/", None, None), + "six" + ); + assert!(no_matches(entries, "https://six.com/", None, None), "six"); + } + + #[track_caller] fn one_match( entries: &[(rbw::db::Entry, DecryptedCipher)], - name: &str, + needle: &str, username: Option<&str>, folder: Option<&str>, idx: usize, ) -> bool { entries_eq( - &find_entry_raw(entries, name, username, folder).unwrap(), + &find_entry_raw( + entries, + &parse_needle(needle).unwrap(), + username, + folder, + ) + .unwrap(), &entries[idx], ) } + #[track_caller] fn no_matches( entries: &[(rbw::db::Entry, DecryptedCipher)], - name: &str, + needle: &str, username: Option<&str>, folder: Option<&str>, ) -> bool { - let res = find_entry_raw(entries, name, username, folder); + let res = find_entry_raw( + entries, + &parse_needle(needle).unwrap(), + username, + folder, + ); if let Err(e) = res { - format!("{}", e).contains("no entry found") + format!("{e}").contains("no entry found") } else { false } } + #[track_caller] fn many_matches( entries: &[(rbw::db::Entry, DecryptedCipher)], - name: &str, + needle: &str, username: Option<&str>, folder: Option<&str>, ) -> bool { - let res = find_entry_raw(entries, name, username, folder); + let res = find_entry_raw( + entries, + &parse_needle(needle).unwrap(), + username, + folder, + ); if let Err(e) = res { - format!("{}", e).contains("multiple entries found") + format!("{e}").contains("multiple entries found") } else { false } } + #[track_caller] fn entries_eq( a: &(rbw::db::Entry, DecryptedCipher), b: &(rbw::db::Entry, DecryptedCipher), @@ -1628,10 +2752,12 @@ mod test { name: &str, username: Option<&str>, folder: Option<&str>, + uris: &[(&str, Option<rbw::api::UriMatchType>)], ) -> (rbw::db::Entry, DecryptedCipher) { + let id = uuid::Uuid::new_v4(); ( rbw::db::Entry { - id: "irrelevant".to_string(), + id: id.to_string(), org_id: None, folder: folder.map(|_| "encrypted folder name".to_string()), folder_id: None, @@ -1641,7 +2767,13 @@ mod test { "this is the encrypted username".to_string() }), password: None, - uris: vec![], + uris: uris + .iter() + .map(|(_, match_type)| rbw::db::Uri { + uri: "this is the encrypted uri".to_string(), + match_type: *match_type, + }) + .collect(), totp: None, }, fields: vec![], @@ -1649,14 +2781,21 @@ mod test { history: vec![], }, DecryptedCipher { - id: "irrelevant".to_string(), + id: id.to_string(), folder: folder.map(std::string::ToString::to_string), name: name.to_string(), data: DecryptedData::Login { username: username.map(std::string::ToString::to_string), password: None, totp: None, - uris: None, + uris: Some( + uris.iter() + .map(|(uri, match_type)| DecryptedUri { + uri: (*uri).to_string(), + match_type: *match_type, + }) + .collect(), + ), }, fields: vec![], notes: None, @@ -1665,13 +2804,3 @@ mod test { ) } } - -fn display_field(name: &str, field: Option<&str>) -> bool { - field.map_or_else( - || false, - |field| { - println!("{}: {}", name, field); - true - }, - ) -} diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 5730298..eefa52a 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -10,25 +10,27 @@ #![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 \ @@ -39,60 +41,68 @@ 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")] - name: String, - #[structopt(help = "Username of the entry to display")] + #[arg(help = "Name, URI or UUID of the entry to display", value_parser = commands::parse_needle)] + needle: commands::Needle, + #[arg(help = "Username of the entry to display")] user: Option<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", + visible_alias = "totp" + )] 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(long, help = "Copy result to clipboard")] + clipboard: bool, }, - #[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 \ @@ -102,28 +112,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", @@ -131,39 +140,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 \ @@ -173,7 +181,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 \ @@ -184,50 +192,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 { @@ -257,20 +263,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, }, } @@ -286,15 +292,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()) } @@ -314,14 +323,33 @@ fn main(opt: Opt) { Opt::Sync => commands::sync(), Opt::List { fields } => commands::list(fields), Opt::Get { - name, + needle, user, folder, + field, full, - } => commands::get(name, user.as_deref(), folder.as_deref(), *full), - Opt::Code { name, user, folder } => { - commands::code(name, user.as_deref(), folder.as_deref()) - } + raw, + clipboard, + } => commands::get( + needle, + user.as_deref(), + folder.as_deref(), + field.as_deref(), + *full, + *raw, + *clipboard, + ), + Opt::Code { + name, + user, + folder, + clipboard, + } => commands::code( + name, + user.as_deref(), + folder.as_deref(), + *clipboard, + ), Opt::Add { name, user, @@ -384,25 +412,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 23ef765..efb1b5f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,10 +8,14 @@ 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, + pub client_cert_path: Option<std::path::PathBuf>, // backcompat, no longer generated in new configs #[serde(skip_serializing)] pub device_id: Option<String>, @@ -23,8 +27,11 @@ impl Default for Config { email: None, base_url: None, identity_url: None, + notifications_url: None, lock_timeout: default_lock_timeout(), + sync_interval: default_sync_interval(), pinentry: default_pinentry(), + client_cert_path: None, device_id: None, } } @@ -36,6 +43,11 @@ pub fn default_lock_timeout() -> u64 { } #[must_use] +pub fn default_sync_interval() -> u64 { + 3600 +} + +#[must_use] pub fn default_pinentry() -> String { "pinentry".to_string() } @@ -134,7 +146,14 @@ impl Config { 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") + } + }, ) } @@ -143,12 +162,41 @@ impl Config { 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() @@ -169,7 +217,7 @@ pub async fn device_id(config: &Config) -> Result<String> { Ok(s.trim().to_string()) } else { let id = config.device_id.as_ref().map_or_else( - || uuid::Uuid::new_v4().to_hyphenated().to_string(), + || uuid::Uuid::new_v4().hyphenated().to_string(), String::to_string, ); let mut fh = tokio::fs::File::create(&file).await.map_err(|e| { @@ -106,7 +106,6 @@ impl<'de> serde::Deserialize<'de> for Uri { #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] -#[allow(clippy::large_enum_variant)] pub enum EntryData { Login { username: Option<String>, @@ -148,8 +147,10 @@ pub enum EntryData { serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub struct Field { + pub ty: crate::api::FieldType, pub name: Option<String>, pub value: Option<String>, + pub linked_id: Option<crate::api::LinkedIdType>, } #[derive( @@ -165,7 +166,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>, @@ -294,6 +298,7 @@ impl Db { 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 5ebeaa2..2fa6e50 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -49,7 +49,7 @@ 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] @@ -79,32 +79,47 @@ pub fn socket_file() -> std::path::PathBuf { #[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(), + rustix::process::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 8f4e534..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,7 +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)] // more to come + #[allow(clippy::single_match_else)] // more to come match editor.file_name() { Some(editor) => match editor.to_str() { Some("vim" | "nvim") => { @@ -53,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() { @@ -81,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 46b242b..db0503a 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 { @@ -19,12 +15,18 @@ pub enum Error { file: std::path::PathBuf, }, + #[error("failed to create reqwest client")] + CreateReqwestClient { source: reqwest::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(), @@ -122,6 +124,12 @@ pub enum Error { file: std::path::PathBuf, }, + #[error("failed to load client cert from {}", .file.display())] + LoadClientCert { + source: tokio::io::Error, + file: std::path::PathBuf, + }, + #[error("invalid padding")] Padding, @@ -131,6 +139,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, @@ -213,6 +227,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..fd46b85 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,8 +12,13 @@ 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 email = email.trim().to_lowercase(); + let iterations = std::num::NonZeroU32::new(iterations) .ok_or(Error::Pbkdf2ZeroIterations)?; @@ -19,12 +26,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 +71,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)?; @@ -10,12 +10,15 @@ #![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_lines)] #![allow(clippy::type_complexity)] +#![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/pinentry.rs b/src/pinentry.rs index ce1227f..e2a83ed 100644 --- a/src/pinentry.rs +++ b/src/pinentry.rs @@ -34,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; @@ -77,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 { @@ -120,7 +118,7 @@ where }); } return Err(Error::PinentryErrorMessage { - error: format!("unknown error ({})", code), + error: format!("unknown error ({code})"), }); } None => { @@ -139,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; } } @@ -162,7 +168,6 @@ 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) { // h and l were parsed from a single hex digit, so they // must be in the range 0-15, so these unwraps are safe diff --git a/src/protocol.rs b/src/protocol.rs index a10d301..e883441 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,6 +1,3 @@ -// 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] @@ -37,6 +34,9 @@ pub enum Action { plaintext: String, org_id: Option<String>, }, + ClipboardStore { + text: String, + }, Quit, Version, } |