aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/tests.yaml67
-rw-r--r--.rustfmt.toml2
-rw-r--r--CHANGELOG.md114
-rw-r--r--Cargo.lock2062
-rw-r--r--Cargo.toml93
-rw-r--r--Makefile17
-rw-r--r--README.md43
-rwxr-xr-xbin/rbw-pinentry-keyring72
-rw-r--r--deny.toml44
-rw-r--r--src/actions.rs111
-rw-r--r--src/api.rs362
-rw-r--r--src/base64.rs15
-rw-r--r--src/bin/rbw-agent/actions.rs285
-rw-r--r--src/bin/rbw-agent/agent.rs197
-rw-r--r--src/bin/rbw-agent/daemon.rs59
-rw-r--r--src/bin/rbw-agent/debugger.rs2
-rw-r--r--src/bin/rbw-agent/main.rs12
-rw-r--r--src/bin/rbw-agent/notifications.rs174
-rw-r--r--src/bin/rbw-agent/sock.rs7
-rw-r--r--src/bin/rbw-agent/timeout.rs66
-rw-r--r--src/bin/rbw/actions.rs48
-rw-r--r--src/bin/rbw/commands.rs1461
-rw-r--r--src/bin/rbw/main.rs213
-rw-r--r--src/cipherstring.rs118
-rw-r--r--src/config.rs54
-rw-r--r--src/db.rs7
-rw-r--r--src/dirs.rs41
-rw-r--r--src/edit.rs20
-rw-r--r--src/error.rs31
-rw-r--r--src/identity.rs53
-rw-r--r--src/lib.rs3
-rw-r--r--src/pinentry.rs23
-rw-r--r--src/protocol.rs6
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
diff --git a/Cargo.lock b/Cargo.lock
index dd8eeaf..87fe839 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 06a6f84..aa45518 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/Makefile b/Makefile
index cedf8b5..509c2ee 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
diff --git a/README.md b/README.md
index 7a7a794..988ac75 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/deny.toml b/deny.toml
index 994369f..df3a3c5 100644
--- a/deny.toml
+++ b/deny.toml
@@ -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))
+}
diff --git a/src/api.rs b/src/api.rs
index 75ab80e..9a58f9e 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -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(&notes, 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| {
diff --git a/src/db.rs b/src/db.rs
index cc9319e..ab742ea 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -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)?;
diff --git a/src/lib.rs b/src/lib.rs
index db615c0..fd14fcf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
}