aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/tests.yaml67
-rw-r--r--.rustfmt.toml2
-rw-r--r--CHANGELOG.md132
-rw-r--r--Cargo.lock2120
-rw-r--r--Cargo.toml98
-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.rs123
-rw-r--r--src/api.rs556
-rw-r--r--src/base64.rs15
-rw-r--r--src/bin/rbw-agent/actions.rs331
-rw-r--r--src/bin/rbw-agent/agent.rs205
-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.rs31
-rw-r--r--src/bin/rbw-agent/notifications.rs174
-rw-r--r--src/bin/rbw-agent/sock.rs5
-rw-r--r--src/bin/rbw-agent/timeout.rs66
-rw-r--r--src/bin/rbw/actions.rs48
-rw-r--r--src/bin/rbw/commands.rs1582
-rw-r--r--src/bin/rbw/main.rs228
-rw-r--r--src/cipherstring.rs118
-rw-r--r--src/config.rs119
-rw-r--r--src/db.rs8
-rw-r--r--src/dirs.rs56
-rw-r--r--src/edit.rs19
-rw-r--r--src/error.rs37
-rw-r--r--src/identity.rs53
-rw-r--r--src/lib.rs19
-rw-r--r--src/locked.rs14
-rw-r--r--src/pinentry.rs29
-rw-r--r--src/protocol.rs7
-rw-r--r--src/pwgen.rs3
35 files changed, 4880 insertions, 1622 deletions
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
new file mode 100644
index 0000000..c02a77b
--- /dev/null
+++ b/.github/workflows/tests.yaml
@@ -0,0 +1,67 @@
+name: tests
+on:
+ push:
+ branches: [main]
+ pull_request: {}
+env:
+ RUST_BACKTRACE: 1
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ - run: cargo build --all-targets
+ build-musl:
+ runs-on: ubuntu-latest
+ steps:
+ - run: sudo apt-get install clang-11
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: x86_64-unknown-linux-musl
+ - run: TARGET_CC=clang-11 TARGET_AR=llvm-ar-11 cargo build --all-targets --target x86_64-unknown-linux-musl
+ build-macos:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ - run: cargo build --all-targets
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ - run: cargo test
+ test-musl:
+ runs-on: ubuntu-latest
+ steps:
+ - run: sudo apt-get install clang-11
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: x86_64-unknown-linux-musl
+ - run: TARGET_CC=clang-11 TARGET_AR=llvm-ar-11 cargo test --target x86_64-unknown-linux-musl
+ test-macos:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ - run: cargo test
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ components: clippy, rustfmt
+ - run: cargo install --locked --debug cargo-deny
+ - run: cargo clippy --all-targets -- -Dwarnings
+ - run: cargo fmt --check
+ - run: cargo deny check
+ doc:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dtolnay/rust-toolchain@stable
+ - run: cargo doc
diff --git a/.rustfmt.toml b/.rustfmt.toml
index 55b0b14..7b6182f 100644
--- a/.rustfmt.toml
+++ b/.rustfmt.toml
@@ -1,2 +1,2 @@
-edition = "2018"
+edition = "2021"
max_width = 78
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23143ec..730b8e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,137 @@
# 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
+
+* Restored packaged scripts to the crate bundle, since they are used by some
+ downstream packages (no functional changes) (#81)
+
+## [1.4.2] - 2022-02-10
+
+### Changed
+
+* Device id is now stored in a separate file in the local data directory
+ instead of as part of the config (#74)
+
+### Fixed
+
+* Fix api renaming in official bitwarden server (#80)
+
## [1.4.1] - 2021-10-28
### Added
diff --git a/Cargo.lock b/Cargo.lock
index 1c11c3d..87fe839 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,52 +3,117 @@
version = 3
[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
name = "aes"
-version = "0.7.5"
+version = "0.8.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.11.0"
+name = "anstream"
+version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
- "winapi",
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
-version = "1.0.44"
+version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
+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.1"
+version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "async-trait"
-version = "0.1.51"
+version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
+checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [
"proc-macro2",
"quote",
@@ -56,27 +121,25 @@ dependencies = [
]
[[package]]
-name = "atty"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
-dependencies = [
- "hermit-abi",
- "libc",
- "winapi",
-]
-
-[[package]]
name = "autocfg"
-version = "0.1.7"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
+checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
-name = "autocfg"
-version = "1.0.1"
+name = "backtrace"
+version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
[[package]]
name = "base32"
@@ -86,15 +149,15 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
[[package]]
name = "base64"
-version = "0.13.0"
+version = "0.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"
@@ -103,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.8.0"
+name = "bytes"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
+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 = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02"
+dependencies = [
+ "calloop",
+ "rustix",
+ "wayland-backend",
+ "wayland-client",
+]
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
[[package]]
name = "cc"
-version = "1.0.71"
+version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
+checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
[[package]]
name = "cfg-if"
@@ -165,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.33.3"
+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 = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+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 = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "copypasta"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b"
+checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858"
+dependencies = [
+ "clipboard-win",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "smithay-clipboard",
+ "x11-clipboard",
+]
[[package]]
name = "core-foundation"
-version = "0.9.2"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
@@ -206,112 +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.4"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28e98c534e9c8a0483aa01d6f6913bc063de254311bd267c9cf535e9b70e15b2"
+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.29"
+name = "dlib"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
+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 = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.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"
@@ -319,43 +541,67 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
-version = "1.0.1"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
- "matches",
"percent-encoding",
]
[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
name = "futures-channel"
-version = "0.3.17"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
+ "futures-sink",
]
[[package]]
name = "futures-core"
-version = "0.3.17"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+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.17"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
-version = "0.3.17"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
- "autocfg 1.0.1",
- "proc-macro-hack",
"proc-macro2",
"quote",
"syn",
@@ -363,124 +609,106 @@ dependencies = [
[[package]]
name = "futures-sink"
-version = "0.3.17"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
-version = "0.3.17"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
-version = "0.3.17"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
- "autocfg 1.0.1",
+ "futures-channel",
"futures-core",
"futures-io",
"futures-macro",
+ "futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
- "proc-macro-hack",
- "proc-macro-nested",
"slab",
]
[[package]]
name = "generic-array"
-version = "0.14.4"
+version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
-name = "getrandom"
-version = "0.2.3"
+name = "gethostname"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
dependencies = [
- "cfg-if",
"libc",
- "wasi",
+ "windows-targets 0.48.5",
]
[[package]]
-name = "h2"
-version = "0.3.7"
+name = "getrandom"
+version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
+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.5"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
@@ -489,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.5.1"
+name = "http-body-util"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
+checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
[[package]]
-name = "httpdate"
-version = "1.0.1"
+name = "httparse"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "humantime"
@@ -518,220 +752,246 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
-version = "0.14.14"
+version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b"
+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.22.1"
+version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64"
+checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
+ "http",
"hyper",
- "log",
+ "hyper-util",
"rustls",
+ "rustls-pki-types",
"tokio",
"tokio-rustls",
- "webpki",
+ "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.7.0"
+name = "idna"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
- "autocfg 1.0.1",
- "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 = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.52.0",
+]
[[package]]
name = "itoa"
-version = "0.4.8"
+version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
-version = "0.3.55"
+version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+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.105"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "libloading"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
+checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.52.5",
+]
[[package]]
name = "libm"
-version = "0.2.1"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
-name = "lock_api"
-version = "0.4.5"
+name = "libredox"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+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.4"
+name = "malloc_buf"
+version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
- "autocfg 1.0.1",
+ "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.7.14"
+name = "memmap2"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+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.0"
+name = "miniz_oxide"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [
- "bitflags",
- "cc",
- "cfg-if",
- "libc",
- "memoffset",
+ "adler",
]
[[package]]
-name = "ntapi"
-version = "0.3.6"
+name = "mio"
+version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+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.7",
"byteorder",
"lazy_static",
"libm",
@@ -745,93 +1005,122 @@ 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.0.1",
"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.0.1",
+ "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.0.1",
+ "autocfg",
"libm",
]
[[package]]
name = "num_cpus"
-version = "1.13.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
-name = "once_cell"
-version = "1.8.0"
+name = "objc"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
[[package]]
-name = "opaque-debug"
-version = "0.3.0"
+name = "objc-foundation"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+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 = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl-probe"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "parking_lot"
-version = "0.11.2"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
- "instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
-version = "0.8.5"
+version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
"cfg-if",
- "instant",
"libc",
"redox_syscall",
"smallvec",
- "winapi",
+ "windows-targets 0.48.5",
]
[[package]]
name = "password-hash"
-version = "0.3.2"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
@@ -839,64 +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.3"
+name = "pin-project-internal"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f22eb0e3c593294a99e9ff4b24cf6b752d43f193aa4415fe5077c159996d497"
+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.7"
+version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
@@ -906,98 +1192,88 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
-version = "0.2.4"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "116bee8279d783c0cf370efa1a94632f2108e5ef0bb32df31f051647810a4e2c"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
- "pem-rfc7468",
- "zeroize",
+ "pkcs8",
+ "spki",
]
[[package]]
name = "pkcs8"
-version = "0.7.6"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
- "pem-rfc7468",
- "pkcs1",
"spki",
- "zeroize",
]
[[package]]
-name = "ppv-lite86"
-version = "0.2.15"
+name = "pkg-config"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+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",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.52.0",
]
[[package]]
-name = "proc-macro-hack"
-version = "0.5.19"
+name = "ppv-lite86"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
-name = "proc-macro-nested"
-version = "0.1.7"
+name = "proc-macro2"
+version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
+checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
+dependencies = [
+ "unicode-ident",
+]
[[package]]
-name = "proc-macro2"
-version = "1.0.32"
+name = "quick-xml"
+version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
+checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
- "unicode-xid",
+ "memchr",
]
[[package]]
name = "quote"
-version = "1.0.10"
+version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
-version = "0.8.4"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
- "rand_hc",
]
[[package]]
@@ -1012,62 +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 = "rand_hc"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
-dependencies = [
- "rand_core",
-]
-
-[[package]]
name = "rbw"
-version = "1.4.1"
+version = "1.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",
@@ -1076,90 +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.6"
+version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280"
+checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
dependencies = [
"base64",
"bytes",
- "encoding_rs",
+ "futures-channel",
"futures-core",
"futures-util",
"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",
@@ -1169,103 +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.19.1"
+version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
+checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
dependencies = [
- "base64",
"log",
"ring",
- "sct",
- "webpki",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
]
[[package]]
name = "rustls-native-certs"
-version = "0.5.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092"
+checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
dependencies = [
"openssl-probe",
- "rustls",
+ "rustls-pemfile",
+ "rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
-name = "ryu"
-version = "1.0.5"
+name = "rustls-pemfile"
+version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64",
+ "rustls-pki-types",
+]
[[package]]
-name = "schannel"
-version = "0.1.19"
+name = "rustls-pki-types"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+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.6.1"
+name = "schannel"
+version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
+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.4.2"
+version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
+checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -1274,9 +1628,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
-version = "2.4.2"
+version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
+checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [
"core-foundation-sys",
"libc",
@@ -1284,18 +1638,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.130"
+version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.130"
+version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
dependencies = [
"proc-macro2",
"quote",
@@ -1304,9 +1658,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.68"
+version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
+checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
dependencies = [
"itoa",
"ryu",
@@ -1315,18 +1669,19 @@ dependencies = [
[[package]]
name = "serde_path_to_error"
-version = "0.1.5"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0421d4f173fab82d72d6babf36d57fae38b994ca5c2d78e704260ba6d12118b"
+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",
@@ -1335,9 +1690,9 @@ dependencies = [
[[package]]
name = "serde_urlencoded"
-version = "0.7.0"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
@@ -1346,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.8"
+version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
- "block-buffer",
"cfg-if",
"cpufeatures",
"digest",
- "opaque-debug",
]
[[package]]
name = "signal-hook-registry"
-version = "1.4.0"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
name = "slab"
-version = "0.4.5"
+version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
[[package]]
name = "smallvec"
-version = "1.7.0"
+version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
-name = "socket2"
-version = "0.4.2"
+name = "smawk"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
+checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+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.25"
+name = "spin"
+version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c"
-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.81"
+version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
+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.2.0"
+version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
- "libc",
- "rand",
- "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",
+ "fastrand",
+ "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",
@@ -1542,44 +1914,43 @@ dependencies = [
[[package]]
name = "tinyvec"
-version = "1.5.0"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.12.0"
+version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
+checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
- "autocfg 1.0.1",
+ "backtrace",
"bytes",
"libc",
- "memchr",
"mio",
"num_cpus",
- "once_cell",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
+ "socket2",
"tokio-macros",
- "winapi",
+ "windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
-version = "1.5.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
@@ -1588,172 +1959,232 @@ dependencies = [
[[package]]
name = "tokio-rustls"
-version = "0.22.0"
+version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
+checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [
"rustls",
+ "rustls-pki-types",
"tokio",
- "webpki",
]
[[package]]
-name = "tokio-util"
-version = "0.6.8"
+name = "tokio-stream"
+version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd"
+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.29"
+version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
- "cfg-if",
+ "log",
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
-version = "0.1.21"
+version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
- "lazy_static",
+ "once_cell",
]
[[package]]
name = "try-lock"
-version = "0.2.3"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+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.14.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-bidi"
-version = "0.3.7"
+version = "0.3.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.8.0"
+name = "unicode-linebreak"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
-name = "unicode-width"
-version = "0.1.9"
+name = "unicode-normalization"
+version = "0.1.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.3"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
- "log",
"try-lock",
]
[[package]]
name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
+version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
-version = "0.2.78"
+version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -1761,13 +2192,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.78"
+version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
- "lazy_static",
"log",
+ "once_cell",
"proc-macro2",
"quote",
"syn",
@@ -1776,9 +2207,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.28"
+version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
dependencies = [
"cfg-if",
"js-sys",
@@ -1788,9 +2219,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.78"
+version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1798,9 +2229,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.78"
+version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
@@ -1811,28 +2242,114 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.78"
+version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
-name = "web-sys"
-version = "0.3.55"
+name = "wayland-backend"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40"
dependencies = [
- "js-sys",
- "wasm-bindgen",
+ "cc",
+ "downcast-rs",
+ "rustix",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
]
[[package]]
-name = "webpki"
-version = "0.21.4"
+name = "wayland-client"
+version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
+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]]
@@ -1852,46 +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 = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+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.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 = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.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.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 = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+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 = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.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 = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+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 = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+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.2"
+name = "x11-clipboard"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970"
+checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286"
dependencies = [
- "zeroize_derive",
+ "libc",
+ "x11rb",
]
[[package]]
-name = "zeroize_derive"
-version = "1.2.0"
+name = "x11rb"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdff2024a851a322b08f179173ae2ba620445aef1e838f0c196820eade4ae0c7"
+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 44575fc..aa45518 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "rbw"
-version = "1.4.1"
+version = "1.10.0"
authors = ["Jesse Luehrs <doy@tozt.net>"]
-edition = "2018"
+edition = "2021"
description = "Unofficial Bitwarden CLI"
repository = "https://git.tozt.net/rbw"
@@ -10,48 +10,62 @@ readme = "README.md"
keywords = ["bitwarden"]
categories = ["command-line-utilities", "cryptography"]
license = "MIT"
+include = ["src/**/*", "bin/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
[dependencies]
-aes = "0.7"
-anyhow = "1.0"
-arrayvec = "0.7"
-async-trait = "0.1"
-base32 = "0.4"
-base64 = "0.13"
-block-modes = "0.8"
-block-padding = "0.2"
-daemonize = "0.4"
-directories = "4.0"
-env_logger = "0.9"
-hkdf = "0.11"
-hmac = { version = "0.11", features = ["std"] }
-humantime = "2.1"
-libc = "0.2"
-log = "0.4"
-nix = "0.23"
-paw = "1.0"
-pbkdf2 = "0.9"
-percent-encoding = "2.1"
-rand = "0.8"
-region = "3.0"
-reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls-native-roots"] }
-rsa = "0.5"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-serde_path_to_error = "0.1"
-serde_repr = "0.1"
-sha-1 = "0.9"
-sha2 = "0.9"
-structopt = { version = "0.3", features = ["paw", "wrap_help"] }
-tempfile = "3.2"
-term_size = "0.3"
-textwrap = "0.11"
-thiserror = "1.0"
-tokio = { version = "1.12", features = ["full"] }
-totp-lite = "1.0"
-url = "2.2"
-uuid = { version = "0.8", features = ["v4"] }
-zeroize = "1.4"
+aes = "0.8.4"
+anyhow = "1.0.82"
+argon2 = "0.5.3"
+arrayvec = "0.7.4"
+async-trait = "0.1.80"
+base32 = "0.4.0"
+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.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 02ec854..7ee1fa4 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -4,11 +4,11 @@ pub async fn register(
email: &str,
apikey: crate::locked::ApiKey,
) -> Result<()> {
- let config = crate::config::Config::load_async().await?;
- let client =
- crate::api::Client::new(&config.base_url(), &config.identity_url());
+ let (client, config) = api_client_async().await?;
- client.register(email, &config.device_id, &apikey).await?;
+ client
+ .register(email, &crate::config::device_id(&config).await?, &apikey)
+ .await?;
Ok(())
}
@@ -18,40 +18,70 @@ pub async fn login(
password: crate::locked::Password,
two_factor_token: Option<&str>,
two_factor_provider: Option<crate::api::TwoFactorProviderType>,
-) -> Result<(String, String, u32, String)> {
- let config = crate::config::Config::load_async().await?;
- let client =
- crate::api::Client::new(&config.base_url(), &config.identity_url());
+) -> Result<(
+ String,
+ String,
+ crate::api::KdfType,
+ u32,
+ Option<u32>,
+ Option<u32>,
+ String,
+)> {
+ let (client, config) = api_client_async().await?;
+ let (kdf, iterations, memory, parallelism) =
+ client.prelogin(email).await?;
- let iterations = client.prelogin(email).await?;
- let identity =
- crate::identity::Identity::new(email, &password, iterations)?;
+ let identity = crate::identity::Identity::new(
+ email,
+ &password,
+ kdf,
+ iterations,
+ memory,
+ parallelism,
+ )?;
let (access_token, refresh_token, protected_key) = client
.login(
email,
- &config.device_id,
+ &crate::config::device_id(&config).await?,
&identity.master_password_hash,
two_factor_token,
two_factor_provider,
)
.await?;
- Ok((access_token, refresh_token, iterations, protected_key))
+ Ok((
+ access_token,
+ refresh_token,
+ kdf,
+ iterations,
+ memory,
+ parallelism,
+ protected_key,
+ ))
}
-pub async fn unlock(
+pub fn unlock<S: std::hash::BuildHasher>(
email: &str,
password: &crate::locked::Password,
+ kdf: crate::api::KdfType,
iterations: u32,
+ memory: Option<u32>,
+ parallelism: Option<u32>,
protected_key: &str,
protected_private_key: &str,
- protected_org_keys: &std::collections::HashMap<String, String>,
+ protected_org_keys: &std::collections::HashMap<String, String, S>,
) -> Result<(
crate::locked::Keys,
std::collections::HashMap<String, crate::locked::Keys>,
)> {
- let identity =
- crate::identity::Identity::new(email, password, iterations)?;
+ let identity = crate::identity::Identity::new(
+ email,
+ password,
+ kdf,
+ iterations,
+ memory,
+ parallelism,
+ )?;
let protected_key =
crate::cipherstring::CipherString::new(protected_key)?;
@@ -119,9 +149,7 @@ async fn sync_once(
std::collections::HashMap<String, String>,
Vec<crate::db::Entry>,
)> {
- let config = crate::config::Config::load_async().await?;
- let client =
- crate::api::Client::new(&config.base_url(), &config.identity_url());
+ let (client, _) = api_client_async().await?;
client.sync(access_token).await
}
@@ -145,10 +173,8 @@ fn add_once(
notes: Option<&str>,
folder_id: Option<&str>,
) -> Result<()> {
- let config = crate::config::Config::load()?;
- let client =
- crate::api::Client::new(&config.base_url(), &config.identity_url());
- client.add(access_token, name, data, notes, folder_id.as_deref())?;
+ let (client, _) = api_client()?;
+ client.add(access_token, name, data, notes, folder_id)?;
Ok(())
}
@@ -159,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],
@@ -170,6 +197,7 @@ pub fn edit(
org_id,
name,
data,
+ fields,
notes,
folder_uuid,
history,
@@ -183,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,
@@ -214,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(())
}
@@ -231,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)
}
@@ -248,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)
}
@@ -300,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 14c11fd..9a58f9e 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -1,9 +1,15 @@
+// serde_repr generates some as conversions that we can't seem to silence from
+// here, unfortunately
+#![allow(clippy::as_conversions)]
+
use crate::prelude::*;
use crate::json::{
DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _,
};
+use tokio::io::AsyncReadExt as _;
+
#[derive(
serde_repr::Serialize_repr,
serde_repr::Deserialize_repr,
@@ -35,7 +41,7 @@ impl std::fmt::Display for UriMatchType {
RegularExpression => "regular_expression",
Never => "never",
};
- write!(f, "{}", s)
+ write!(f, "{s}")
}
}
@@ -51,6 +57,33 @@ pub enum TwoFactorProviderType {
WebAuthn = 7,
}
+impl TwoFactorProviderType {
+ #[must_use]
+ pub fn message(&self) -> &str {
+ match *self {
+ Self::Authenticator => "Enter the 6 digit verification code from your authenticator app.",
+ Self::Yubikey => "Insert your Yubikey and push the button.",
+ Self::Email => "Enter the PIN you received via email.",
+ _ => "Enter the code."
+ }
+ }
+
+ #[must_use]
+ pub fn header(&self) -> &str {
+ match *self {
+ Self::Authenticator => "Authenticator App",
+ Self::Yubikey => "Yubikey",
+ Self::Email => "Email Code",
+ _ => "Two Factor Authentication",
+ }
+ }
+
+ #[must_use]
+ pub fn grab(&self) -> bool {
+ !matches!(self, Self::Email)
+ }
+}
+
impl<'de> serde::Deserialize<'de> for TwoFactorProviderType {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
@@ -107,7 +140,7 @@ impl std::convert::TryFrom<u64> for TwoFactorProviderType {
6 => Ok(Self::OrganizationDuo),
7 => Ok(Self::WebAuthn),
_ => Err(Error::InvalidTwoFactorProvider {
- ty: format!("{}", ty),
+ ty: format!("{ty}"),
}),
}
}
@@ -131,6 +164,96 @@ impl std::str::FromStr for TwoFactorProviderType {
}
}
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum KdfType {
+ Pbkdf2 = 0,
+ Argon2id = 1,
+}
+
+impl<'de> serde::Deserialize<'de> for KdfType {
+ fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct KdfTypeVisitor;
+ impl<'de> serde::de::Visitor<'de> for KdfTypeVisitor {
+ type Value = KdfType;
+
+ fn expecting(
+ &self,
+ formatter: &mut std::fmt::Formatter,
+ ) -> std::fmt::Result {
+ formatter.write_str("kdf id")
+ }
+
+ fn visit_str<E>(
+ self,
+ value: &str,
+ ) -> std::result::Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ value.parse().map_err(serde::de::Error::custom)
+ }
+
+ fn visit_u64<E>(
+ self,
+ value: u64,
+ ) -> std::result::Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ std::convert::TryFrom::try_from(value)
+ .map_err(serde::de::Error::custom)
+ }
+ }
+
+ deserializer.deserialize_any(KdfTypeVisitor)
+ }
+}
+
+impl std::convert::TryFrom<u64> for KdfType {
+ type Error = Error;
+
+ fn try_from(ty: u64) -> Result<Self> {
+ match ty {
+ 0 => Ok(Self::Pbkdf2),
+ 1 => Ok(Self::Argon2id),
+ _ => Err(Error::InvalidKdfType {
+ ty: format!("{ty}"),
+ }),
+ }
+ }
+}
+
+impl std::str::FromStr for KdfType {
+ type Err = Error;
+
+ fn from_str(ty: &str) -> Result<Self> {
+ match ty {
+ "0" => Ok(Self::Pbkdf2),
+ "1" => Ok(Self::Argon2id),
+ _ => Err(Error::InvalidKdfType { ty: ty.to_string() }),
+ }
+ }
+}
+
+impl serde::Serialize for KdfType {
+ fn serialize<S>(
+ &self,
+ serializer: S,
+ ) -> std::result::Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let s = match self {
+ Self::Pbkdf2 => "0",
+ Self::Argon2id => "1",
+ };
+ serializer.serialize_str(s)
+ }
+}
+
#[derive(serde::Serialize, Debug)]
struct PreloginReq {
email: String,
@@ -138,10 +261,14 @@ struct PreloginReq {
#[derive(serde::Deserialize, Debug)]
struct PreloginRes {
- #[serde(rename = "Kdf")]
- kdf: u32,
- #[serde(rename = "KdfIterations")]
+ #[serde(rename = "Kdf", alias = "kdf")]
+ kdf: KdfType,
+ #[serde(rename = "KdfIterations", alias = "kdfIterations")]
kdf_iterations: u32,
+ #[serde(rename = "KdfMemory", alias = "kdfMemory")]
+ kdf_memory: Option<u32>,
+ #[serde(rename = "KdfParallelism", alias = "kdfParallelism")]
+ kdf_parallelism: Option<u32>,
}
#[derive(serde::Serialize, Debug)]
@@ -169,10 +296,8 @@ struct ConnectPasswordReq {
#[derive(serde::Deserialize, Debug)]
struct ConnectPasswordRes {
access_token: String,
- expires_in: u32,
- token_type: String,
refresh_token: String,
- #[serde(rename = "Key")]
+ #[serde(rename = "Key", alias = "key")]
key: String,
}
@@ -180,15 +305,15 @@ struct ConnectPasswordRes {
struct ConnectErrorRes {
error: String,
error_description: Option<String>,
- #[serde(rename = "ErrorModel")]
+ #[serde(rename = "ErrorModel", alias = "errorModel")]
error_model: Option<ConnectErrorResErrorModel>,
- #[serde(rename = "TwoFactorProviders")]
+ #[serde(rename = "TwoFactorProviders", alias = "twoFactorProviders")]
two_factor_providers: Option<Vec<TwoFactorProviderType>>,
}
#[derive(serde::Deserialize, Debug)]
struct ConnectErrorResErrorModel {
- #[serde(rename = "Message")]
+ #[serde(rename = "Message", alias = "message")]
message: String,
}
@@ -202,46 +327,43 @@ struct ConnectRefreshTokenReq {
#[derive(serde::Deserialize, Debug)]
struct ConnectRefreshTokenRes {
access_token: String,
- expires_in: u32,
- token_type: String,
- refresh_token: String,
}
#[derive(serde::Deserialize, Debug)]
struct SyncRes {
- #[serde(rename = "Ciphers")]
+ #[serde(rename = "Ciphers", alias = "ciphers")]
ciphers: Vec<SyncResCipher>,
- #[serde(rename = "Profile")]
+ #[serde(rename = "Profile", alias = "profile")]
profile: SyncResProfile,
- #[serde(rename = "Folders")]
+ #[serde(rename = "Folders", alias = "folders")]
folders: Vec<SyncResFolder>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct SyncResCipher {
- #[serde(rename = "Id")]
+ #[serde(rename = "Id", alias = "id")]
id: String,
- #[serde(rename = "FolderId")]
+ #[serde(rename = "FolderId", alias = "folderId")]
folder_id: Option<String>,
- #[serde(rename = "OrganizationId")]
+ #[serde(rename = "OrganizationId", alias = "organizationId")]
organization_id: Option<String>,
- #[serde(rename = "Name")]
+ #[serde(rename = "Name", alias = "name")]
name: String,
- #[serde(rename = "Login")]
+ #[serde(rename = "Login", alias = "login")]
login: Option<CipherLogin>,
- #[serde(rename = "Card")]
+ #[serde(rename = "Card", alias = "card")]
card: Option<CipherCard>,
- #[serde(rename = "Identity")]
+ #[serde(rename = "Identity", alias = "identity")]
identity: Option<CipherIdentity>,
- #[serde(rename = "SecureNote")]
+ #[serde(rename = "SecureNote", alias = "secureNote")]
secure_note: Option<CipherSecureNote>,
- #[serde(rename = "Notes")]
+ #[serde(rename = "Notes", alias = "notes")]
notes: Option<String>,
- #[serde(rename = "PasswordHistory")]
+ #[serde(rename = "PasswordHistory", alias = "passwordHistory")]
password_history: Option<Vec<SyncResPasswordHistory>>,
- #[serde(rename = "Fields")]
- fields: Option<Vec<SyncResField>>,
- #[serde(rename = "DeletedDate")]
+ #[serde(rename = "Fields", alias = "fields")]
+ fields: Option<Vec<CipherField>>,
+ #[serde(rename = "DeletedDate", alias = "deletedDate")]
deleted_date: Option<String>,
}
@@ -253,32 +375,37 @@ impl SyncResCipher {
if self.deleted_date.is_some() {
return None;
}
- let history = if let Some(history) = &self.password_history {
- history
- .iter()
- .filter_map(|entry| {
- // Gets rid of entries with a non-existent password
- entry.password.clone().map(|p| crate::db::HistoryEntry {
- last_used_date: entry.last_used_date.clone(),
- password: p,
- })
- })
- .collect()
- } else {
- vec![]
- };
+ let history =
+ self.password_history
+ .as_ref()
+ .map_or_else(Vec::new, |history| {
+ history
+ .iter()
+ .filter_map(|entry| {
+ // Gets rid of entries with a non-existent
+ // password
+ entry.password.clone().map(|p| {
+ crate::db::HistoryEntry {
+ last_used_date: entry
+ .last_used_date
+ .clone(),
+ password: p,
+ }
+ })
+ })
+ .collect()
+ });
- let (folder, folder_id) = if let Some(folder_id) = &self.folder_id {
- let mut folder_name = None;
- for folder in folders {
- if &folder.id == folder_id {
- folder_name = Some(folder.name.clone());
+ let (folder, folder_id) =
+ self.folder_id.as_ref().map_or((None, None), |folder_id| {
+ let mut folder_name = None;
+ for folder in folders {
+ if &folder.id == folder_id {
+ folder_name = Some(folder.name.clone());
+ }
}
- }
- (folder_name, Some(folder_id))
- } else {
- (None, None)
- };
+ (folder_name, Some(folder_id))
+ });
let data = if let Some(login) = &self.login {
crate::db::EntryData::Login {
username: login.username.clone(),
@@ -332,17 +459,17 @@ impl SyncResCipher {
} else {
return None;
};
- let fields = if let Some(fields) = &self.fields {
+ let fields = self.fields.as_ref().map_or_else(Vec::new, |fields| {
fields
.iter()
.map(|field| crate::db::Field {
+ ty: field.ty,
name: field.name.clone(),
value: field.value.clone(),
+ linked_id: field.linked_id,
})
.collect()
- } else {
- vec![]
- };
+ });
Some(crate::db::Entry {
id: self.id.clone(),
org_id: self.organization_id.clone(),
@@ -359,104 +486,173 @@ impl SyncResCipher {
#[derive(serde::Deserialize, Debug)]
struct SyncResProfile {
- #[serde(rename = "Key")]
+ #[serde(rename = "Key", alias = "key")]
key: String,
- #[serde(rename = "PrivateKey")]
+ #[serde(rename = "PrivateKey", alias = "privateKey")]
private_key: String,
- #[serde(rename = "Organizations")]
+ #[serde(rename = "Organizations", alias = "organizations")]
organizations: Vec<SyncResProfileOrganization>,
}
#[derive(serde::Deserialize, Debug)]
struct SyncResProfileOrganization {
- #[serde(rename = "Id")]
+ #[serde(rename = "Id", alias = "id")]
id: String,
- #[serde(rename = "Key")]
+ #[serde(rename = "Key", alias = "key")]
key: String,
}
#[derive(serde::Deserialize, Debug, Clone)]
struct SyncResFolder {
- #[serde(rename = "Id")]
+ #[serde(rename = "Id", alias = "id")]
id: String,
- #[serde(rename = "Name")]
+ #[serde(rename = "Name", alias = "name")]
name: String,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct CipherLogin {
- #[serde(rename = "Username")]
+ #[serde(rename = "Username", alias = "username")]
username: Option<String>,
- #[serde(rename = "Password")]
+ #[serde(rename = "Password", alias = "password")]
password: Option<String>,
- #[serde(rename = "Totp")]
+ #[serde(rename = "Totp", alias = "totp")]
totp: Option<String>,
- #[serde(rename = "Uris")]
+ #[serde(rename = "Uris", alias = "uris")]
uris: Option<Vec<CipherLoginUri>>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct CipherLoginUri {
- #[serde(rename = "Uri")]
+ #[serde(rename = "Uri", alias = "uri")]
uri: Option<String>,
- #[serde(rename = "Match")]
+ #[serde(rename = "Match", alias = "match")]
match_type: Option<UriMatchType>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct CipherCard {
- #[serde(rename = "CardholderName")]
+ #[serde(rename = "CardholderName", alias = "cardHolderName")]
cardholder_name: Option<String>,
- #[serde(rename = "Number")]
+ #[serde(rename = "Number", alias = "number")]
number: Option<String>,
- #[serde(rename = "Brand")]
+ #[serde(rename = "Brand", alias = "brand")]
brand: Option<String>,
- #[serde(rename = "ExpMonth")]
+ #[serde(rename = "ExpMonth", alias = "expMonth")]
exp_month: Option<String>,
- #[serde(rename = "ExpYear")]
+ #[serde(rename = "ExpYear", alias = "expYear")]
exp_year: Option<String>,
- #[serde(rename = "Code")]
+ #[serde(rename = "Code", alias = "code")]
code: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct CipherIdentity {
- #[serde(rename = "Title")]
+ #[serde(rename = "Title", alias = "title")]
title: Option<String>,
- #[serde(rename = "FirstName")]
+ #[serde(rename = "FirstName", alias = "firstName")]
first_name: Option<String>,
- #[serde(rename = "MiddleName")]
+ #[serde(rename = "MiddleName", alias = "middleName")]
middle_name: Option<String>,
- #[serde(rename = "LastName")]
+ #[serde(rename = "LastName", alias = "lastName")]
last_name: Option<String>,
- #[serde(rename = "Address1")]
+ #[serde(rename = "Address1", alias = "address1")]
address1: Option<String>,
- #[serde(rename = "Address2")]
+ #[serde(rename = "Address2", alias = "address2")]
address2: Option<String>,
- #[serde(rename = "Address3")]
+ #[serde(rename = "Address3", alias = "address3")]
address3: Option<String>,
- #[serde(rename = "City")]
+ #[serde(rename = "City", alias = "city")]
city: Option<String>,
- #[serde(rename = "State")]
+ #[serde(rename = "State", alias = "state")]
state: Option<String>,
- #[serde(rename = "PostalCode")]
+ #[serde(rename = "PostalCode", alias = "postalCode")]
postal_code: Option<String>,
- #[serde(rename = "Country")]
+ #[serde(rename = "Country", alias = "country")]
country: Option<String>,
- #[serde(rename = "Phone")]
+ #[serde(rename = "Phone", alias = "phone")]
phone: Option<String>,
- #[serde(rename = "Email")]
+ #[serde(rename = "Email", alias = "email")]
email: Option<String>,
- #[serde(rename = "SSN")]
+ #[serde(rename = "SSN", alias = "ssn")]
ssn: Option<String>,
- #[serde(rename = "LicenseNumber")]
+ #[serde(rename = "LicenseNumber", alias = "licenseNumber")]
license_number: Option<String>,
- #[serde(rename = "PassportNumber")]
+ #[serde(rename = "PassportNumber", alias = "passportNumber")]
passport_number: Option<String>,
- #[serde(rename = "Username")]
+ #[serde(rename = "Username", alias = "username")]
username: Option<String>,
}
+#[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)]
@@ -464,22 +660,12 @@ struct CipherSecureNote {}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct SyncResPasswordHistory {
- #[serde(rename = "LastUsedDate")]
+ #[serde(rename = "LastUsedDate", alias = "lastUsedDate")]
last_used_date: String,
- #[serde(rename = "Password")]
+ #[serde(rename = "Password", alias = "password")]
password: Option<String>,
}
-#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
-struct SyncResField {
- #[serde(rename = "Type")]
- ty: u32,
- #[serde(rename = "Name")]
- name: Option<String>,
- #[serde(rename = "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")]
@@ -530,15 +717,15 @@ struct CiphersPutReqHistory {
#[derive(serde::Deserialize, Debug)]
struct FoldersRes {
- #[serde(rename = "Data")]
+ #[serde(rename = "Data", alias = "data")]
data: Vec<FoldersResData>,
}
#[derive(serde::Deserialize, Debug)]
struct FoldersResData {
- #[serde(rename = "Id")]
+ #[serde(rename = "Id", alias = "id")]
id: String,
- #[serde(rename = "Name")]
+ #[serde(rename = "Name", alias = "name")]
name: String,
}
@@ -551,21 +738,70 @@ struct FoldersPostReq {
pub struct Client {
base_url: String,
identity_url: String,
+ client_cert_path: Option<std::path::PathBuf>,
}
impl Client {
- pub fn new(base_url: &str, identity_url: &str) -> Self {
+ #[must_use]
+ pub fn new(
+ base_url: &str,
+ identity_url: &str,
+ client_cert_path: Option<&std::path::Path>,
+ ) -> Self {
Self {
base_url: base_url.to_string(),
identity_url: identity_url.to_string(),
+ client_cert_path: client_cert_path
+ .map(std::path::Path::to_path_buf),
+ }
+ }
+
+ 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)
@@ -573,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(
@@ -596,22 +837,34 @@ impl Client {
device_type: 8,
device_identifier: device_id.to_string(),
device_name: "rbw".to_string(),
- device_push_token: "".to_string(),
+ device_push_token: String::new(),
two_factor_token: None,
two_factor_provider: None,
};
- let client = reqwest::Client::new();
+ let client = self.reqwest_client().await?;
let res = client
.post(&self.identity_url("/connect/token"))
.form(&connect_req)
.send()
.await
.map_err(|source| Error::Reqwest { source })?;
- if let reqwest::StatusCode::OK = res.status() {
+ if res.status() == reqwest::StatusCode::OK {
Ok(())
} else {
let code = res.status().as_u16();
- Err(classify_login_error(&res.json_with_path().await?, code))
+ match res.text().await {
+ Ok(body) => match body.clone().json_with_path() {
+ Ok(json) => Err(classify_login_error(&json, code)),
+ Err(e) => {
+ log::warn!("{e}: {body}");
+ Err(Error::RequestFailed { status: code })
+ }
+ },
+ Err(e) => {
+ log::warn!("failed to read response body: {e}");
+ Err(Error::RequestFailed { status: code })
+ }
+ }
}
}
@@ -626,30 +879,30 @@ impl Client {
let connect_req = ConnectPasswordReq {
grant_type: "password".to_string(),
username: email.to_string(),
- password: Some(base64::encode(password_hash.hash())),
+ password: Some(crate::base64::encode(password_hash.hash())),
scope: "api offline_access".to_string(),
client_id: "desktop".to_string(),
client_secret: None,
device_type: 8,
device_identifier: device_id.to_string(),
device_name: "rbw".to_string(),
- device_push_token: "".to_string(),
+ device_push_token: String::new(),
two_factor_token: two_factor_token
.map(std::string::ToString::to_string),
two_factor_provider: two_factor_provider.map(|ty| ty as u32),
};
- let client = reqwest::Client::new();
+ let client = self.reqwest_client().await?;
let res = client
.post(&self.identity_url("/connect/token"))
.form(&connect_req)
.header(
"auth-email",
- base64::encode_config(email, base64::URL_SAFE_NO_PAD),
+ crate::base64::encode_url_safe_no_pad(email),
)
.send()
.await
.map_err(|source| Error::Reqwest { source })?;
- if let reqwest::StatusCode::OK = res.status() {
+ if res.status() == reqwest::StatusCode::OK {
let connect_res: ConnectPasswordRes =
res.json_with_path().await?;
Ok((
@@ -659,7 +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 })
+ }
+ }
}
}
@@ -672,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 })?;
@@ -816,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 })?;
@@ -839,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(),
@@ -853,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 {
@@ -949,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 })?;
@@ -968,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() {
@@ -989,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() {
@@ -1021,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 })?;
@@ -1051,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 })?;
@@ -1068,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 1cc71c3..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,
@@ -61,10 +59,9 @@ pub async fn register(
message,
})
.context("failed to log in to bitwarden instance");
- } else {
- err_msg = Some(message);
- continue;
}
+ err_msg = Some(message);
+ continue;
}
Err(e) => {
return Err(e)
@@ -81,7 +78,7 @@ pub async fn register(
pub async fn login(
sock: &mut crate::sock::Sock,
- state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>,
+ state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>,
tty: Option<&str>,
) -> anyhow::Result<()> {
let db = load_db().await.unwrap_or_else(|_| rbw::db::Db::new());
@@ -90,9 +87,7 @@ pub async fn login(
let url_str = config_base_url().await?;
let url = reqwest::Url::parse(&url_str)
.context("failed to parse base url")?;
- let host = if let Some(host) = url.host_str() {
- host
- } else {
+ let Some(host) = url.host_str() else {
return Err(anyhow::anyhow!(
"couldn't find host in rbw base url {}",
url_str
@@ -102,7 +97,7 @@ pub async fn login(
let email = config_email().await?;
let mut err_msg = None;
- for i in 1_u8..=3 {
+ 'attempts: for i in 1_u8..=3 {
let err = if i > 1 {
// this unwrap is safe because we only ever continue the loop
// if we have set err_msg
@@ -113,7 +108,7 @@ pub async fn login(
let password = rbw::pinentry::getpin(
&config_pinentry().await?,
"Master Password",
- &format!("Log in to {}", host),
+ &format!("Log in to {host}"),
err.as_deref(),
tty,
true,
@@ -126,55 +121,70 @@ pub async fn login(
Ok((
access_token,
refresh_token,
+ kdf,
iterations,
+ memory,
+ parallelism,
protected_key,
)) => {
login_success(
- sock,
- state,
+ state.clone(),
access_token,
refresh_token,
+ kdf,
iterations,
+ memory,
+ parallelism,
protected_key,
password,
db,
email,
)
.await?;
- break;
+ break 'attempts;
}
Err(rbw::error::Error::TwoFactorRequired { providers }) => {
- if providers.contains(
- &rbw::api::TwoFactorProviderType::Authenticator,
- ) {
- let (
- access_token,
- refresh_token,
- iterations,
- protected_key,
- ) = two_factor(
- tty,
- &email,
- password.clone(),
- rbw::api::TwoFactorProviderType::Authenticator,
- )
- .await?;
- login_success(
- sock,
- state,
- access_token,
- refresh_token,
- iterations,
- protected_key,
- password,
- db,
- email,
- )
- .await?;
- break;
- } else {
- return Err(anyhow::anyhow!("TODO"));
+ let supported_types = vec![
+ rbw::api::TwoFactorProviderType::Authenticator,
+ rbw::api::TwoFactorProviderType::Yubikey,
+ rbw::api::TwoFactorProviderType::Email,
+ ];
+
+ for provider in supported_types {
+ if providers.contains(&provider) {
+ let (
+ access_token,
+ refresh_token,
+ kdf,
+ iterations,
+ memory,
+ parallelism,
+ protected_key,
+ ) = two_factor(
+ tty,
+ &email,
+ password.clone(),
+ provider,
+ )
+ .await?;
+ login_success(
+ state.clone(),
+ access_token,
+ refresh_token,
+ kdf,
+ iterations,
+ memory,
+ parallelism,
+ protected_key,
+ password,
+ db,
+ email,
+ )
+ .await?;
+ break 'attempts;
+ }
}
+ return Err(anyhow::anyhow!("TODO"));
}
Err(rbw::error::Error::IncorrectPassword { message }) => {
if i == 3 {
@@ -182,10 +192,9 @@ pub async fn login(
message,
})
.context("failed to log in to bitwarden instance");
- } else {
- err_msg = Some(message);
- continue;
}
+ err_msg = Some(message);
+ continue;
}
Err(e) => {
return Err(e)
@@ -205,7 +214,15 @@ async fn two_factor(
email: &str,
password: rbw::locked::Password,
provider: rbw::api::TwoFactorProviderType,
-) -> anyhow::Result<(String, String, u32, String)> {
+) -> anyhow::Result<(
+ String,
+ String,
+ rbw::api::KdfType,
+ u32,
+ Option<u32>,
+ Option<u32>,
+ String,
+)> {
let mut err_msg = None;
for i in 1_u8..=3 {
let err = if i > 1 {
@@ -217,11 +234,11 @@ async fn two_factor(
};
let code = rbw::pinentry::getpin(
&config_pinentry().await?,
- "Authenticator App",
- "Enter the 6 digit verification code from your authenticator app.",
+ provider.header(),
+ provider.message(),
err.as_deref(),
tty,
- true,
+ provider.grab(),
)
.await
.context("failed to read code from pinentry")?;
@@ -235,11 +252,22 @@ async fn two_factor(
)
.await
{
- Ok((access_token, refresh_token, iterations, protected_key)) => {
+ Ok((
+ access_token,
+ refresh_token,
+ kdf,
+ iterations,
+ memory,
+ parallelism,
+ protected_key,
+ )) => {
return Ok((
access_token,
refresh_token,
+ kdf,
iterations,
+ memory,
+ parallelism,
protected_key,
))
}
@@ -249,10 +277,9 @@ async fn two_factor(
message,
})
.context("failed to log in to bitwarden instance");
- } else {
- err_msg = Some(message);
- continue;
}
+ err_msg = Some(message);
+ continue;
}
// can get this if the user passes an empty string
Err(rbw::error::Error::TwoFactorRequired { .. }) => {
@@ -262,10 +289,9 @@ async fn two_factor(
message,
})
.context("failed to log in to bitwarden instance");
- } else {
- err_msg = Some(message);
- continue;
}
+ err_msg = Some(message);
+ continue;
}
Err(e) => {
return Err(e)
@@ -278,11 +304,13 @@ async fn two_factor(
}
async fn login_success(
- sock: &mut crate::sock::Sock,
- state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>,
+ state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>,
access_token: String,
refresh_token: String,
+ kdf: rbw::api::KdfType,
iterations: u32,
+ memory: Option<u32>,
+ parallelism: Option<u32>,
protected_key: String,
password: rbw::locked::Password,
mut db: rbw::db::Db,
@@ -290,35 +318,37 @@ async fn login_success(
) -> anyhow::Result<()> {
db.access_token = Some(access_token.to_string());
db.refresh_token = Some(refresh_token.to_string());
+ db.kdf = Some(kdf);
db.iterations = Some(iterations);
+ db.memory = memory;
+ db.parallelism = parallelism;
db.protected_key = Some(protected_key.to_string());
save_db(&db).await?;
- sync(sock, false).await?;
+ sync(None, state.clone()).await?;
let db = load_db().await?;
- let protected_private_key =
- if let Some(protected_private_key) = db.protected_private_key {
- protected_private_key
- } else {
- return Err(anyhow::anyhow!(
- "failed to find protected private key in db"
- ));
- };
+ let Some(protected_private_key) = db.protected_private_key else {
+ return Err(anyhow::anyhow!(
+ "failed to find protected private key in db"
+ ));
+ };
let res = rbw::actions::unlock(
&email,
&password,
+ kdf,
iterations,
+ memory,
+ parallelism,
&protected_key,
&protected_private_key,
&db.protected_org_keys,
- )
- .await;
+ );
match res {
Ok((keys, org_keys)) => {
- let mut state = state.write().await;
+ let mut state = state.lock().await;
state.priv_key = Some(keys);
state.org_keys = Some(org_keys);
}
@@ -330,39 +360,40 @@ async fn login_success(
pub async fn unlock(
sock: &mut crate::sock::Sock,
- state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>,
+ state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>,
tty: Option<&str>,
) -> anyhow::Result<()> {
- if state.read().await.needs_unlock() {
+ if state.lock().await.needs_unlock() {
let db = load_db().await?;
- let iterations = if let Some(iterations) = db.iterations {
- iterations
- } else {
+ let Some(kdf) = db.kdf else {
+ return Err(anyhow::anyhow!("failed to find kdf type in db"));
+ };
+
+ let Some(iterations) = db.iterations else {
return Err(anyhow::anyhow!(
"failed to find number of iterations in db"
));
};
- let protected_key = if let Some(protected_key) = db.protected_key {
- protected_key
- } else {
+
+ let memory = db.memory;
+ let parallelism = db.parallelism;
+
+ let Some(protected_key) = db.protected_key else {
return Err(anyhow::anyhow!(
"failed to find protected key in db"
));
};
- let protected_private_key =
- if let Some(protected_private_key) = db.protected_private_key {
- protected_private_key
- } else {
- return Err(anyhow::anyhow!(
- "failed to find protected private key in db"
- ));
- };
+ let Some(protected_private_key) = db.protected_private_key else {
+ return Err(anyhow::anyhow!(
+ "failed to find protected private key in db"
+ ));
+ };
let email = config_email().await?;
let mut err_msg = None;
- for i in 1u8..=3 {
+ for i in 1_u8..=3 {
let err = if i > 1 {
// this unwrap is safe because we only ever continue the loop
// if we have set err_msg
@@ -373,7 +404,10 @@ pub async fn unlock(
let password = rbw::pinentry::getpin(
&config_pinentry().await?,
"Master Password",
- "Unlock the local database",
+ &format!(
+ "Unlock the local database for '{}'",
+ rbw::dirs::profile()
+ ),
err.as_deref(),
tty,
true,
@@ -383,13 +417,14 @@ pub async fn unlock(
match rbw::actions::unlock(
&email,
&password,
+ kdf,
iterations,
+ memory,
+ parallelism,
&protected_key,
&protected_private_key,
&db.protected_org_keys,
- )
- .await
- {
+ ) {
Ok((keys, org_keys)) => {
unlock_success(state, keys, org_keys).await?;
break;
@@ -400,10 +435,9 @@ pub async fn unlock(
message,
})
.context("failed to unlock database");
- } else {
- err_msg = Some(message);
- continue;
}
+ err_msg = Some(message);
+ continue;
}
Err(e) => return Err(e).context("failed to unlock database"),
}
@@ -416,11 +450,11 @@ pub async fn unlock(
}
async fn unlock_success(
- state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>,
+ state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>,
keys: rbw::locked::Keys,
org_keys: std::collections::HashMap<String, rbw::locked::Keys>,
) -> anyhow::Result<()> {
- let mut state = state.write().await;
+ let mut state = state.lock().await;
state.priv_key = Some(keys);
state.org_keys = Some(org_keys);
Ok(())
@@ -428,9 +462,9 @@ async fn unlock_success(
pub async fn lock(
sock: &mut crate::sock::Sock,
- state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>,
+ state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>,
) -> anyhow::Result<()> {
- state.write().await.clear();
+ state.lock().await.clear();
respond_ack(sock).await?;
@@ -439,10 +473,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"));
}
@@ -452,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?;
@@ -482,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?;
}
@@ -491,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"
));
@@ -519,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"
));
@@ -542,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(),
@@ -579,11 +631,10 @@ async fn respond_encrypt(
async fn config_email() -> anyhow::Result<String> {
let config = rbw::config::Config::load_async().await?;
- if let Some(email) = config.email {
- Ok(email)
- } else {
- Err(anyhow::anyhow!("failed to find email address in config"))
- }
+ config.email.map_or_else(
+ || Err(anyhow::anyhow!("failed to find email address in config")),
+ Ok,
+ )
}
async fn load_db() -> anyhow::Result<rbw::db::Db> {
@@ -617,3 +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 fae8c7b..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,103 +27,163 @@ impl State {
}
pub fn set_timeout(&mut self) {
- // no real better option to unwrap here
- self.timeout_chan.send(TimeoutEvent::Set).unwrap();
+ self.timeout.set(self.timeout_duration);
}
pub fn clear(&mut self) {
self.priv_key = None;
- self.org_keys = Default::default();
- // no real better option to unwrap here
- self.timeout_chan.send(TimeoutEvent::Clear).unwrap();
+ self.org_keys = None;
+ self.timeout.clear();
+ }
+
+ pub fn set_sync_timeout(&mut self) {
+ self.sync_timeout.set(self.sync_timeout_duration);
}
}
pub struct Agent {
- timeout_duration: tokio::time::Duration,
- timeout: Option<std::pin::Pin<Box<tokio::time::Sleep>>>,
- timeout_chan: tokio::sync::mpsc::UnboundedReceiver<TimeoutEvent>,
- state: std::sync::Arc<tokio::sync::RwLock<State>>,
+ timer_r: tokio::sync::mpsc::UnboundedReceiver<()>,
+ sync_timer_r: tokio::sync::mpsc::UnboundedReceiver<()>,
+ state: std::sync::Arc<tokio::sync::Mutex<State>>,
}
impl Agent {
pub fn new() -> anyhow::Result<Self> {
let config = rbw::config::Config::load()?;
let timeout_duration =
- tokio::time::Duration::from_secs(config.lock_timeout);
- let (w, r) = tokio::sync::mpsc::unbounded_channel();
+ std::time::Duration::from_secs(config.lock_timeout);
+ let sync_timeout_duration =
+ std::time::Duration::from_secs(config.sync_interval);
+ let (timeout, timer_r) = crate::timeout::Timeout::new();
+ let (sync_timeout, sync_timer_r) = crate::timeout::Timeout::new();
+ if sync_timeout_duration > std::time::Duration::ZERO {
+ sync_timeout.set(sync_timeout_duration);
+ }
+ let notifications_handler = crate::notifications::Handler::new();
+ let clipboard: Box<dyn copypasta::ClipboardProvider> =
+ copypasta::ClipboardContext::new().map_or_else(
+ |e| {
+ log::warn!("couldn't create clipboard context: {e}");
+ let clipboard = Box::new(
+ // infallible
+ copypasta::nop_clipboard::NopClipboardContext::new()
+ .unwrap(),
+ );
+ let clipboard: Box<dyn copypasta::ClipboardProvider> =
+ clipboard;
+ clipboard
+ },
+ |clipboard| {
+ let clipboard = Box::new(clipboard);
+ let clipboard: Box<dyn copypasta::ClipboardProvider> =
+ clipboard;
+ clipboard
+ },
+ );
Ok(Self {
- timeout_duration,
- timeout: None,
- timeout_chan: r,
- state: std::sync::Arc::new(tokio::sync::RwLock::new(State {
+ timer_r,
+ sync_timer_r,
+ state: std::sync::Arc::new(tokio::sync::Mutex::new(State {
priv_key: None,
- org_keys: Default::default(),
- timeout_chan: w,
+ org_keys: None,
+ timeout,
+ timeout_duration,
+ sync_timeout,
+ sync_timeout_duration,
+ notifications_handler,
+ clipboard,
})),
})
}
- fn set_timeout(&mut self) {
- self.timeout =
- Some(Box::pin(tokio::time::sleep(self.timeout_duration)));
- }
-
- fn clear_timeout(&mut self) {
- self.timeout = None;
- }
-
pub async fn run(
- &mut self,
+ self,
listener: tokio::net::UnixListener,
) -> anyhow::Result<()> {
- // tokio only supports timeouts up to 2^36 milliseconds
- let mut forever = Box::pin(tokio::time::sleep(
- tokio::time::Duration::from_secs(60 * 60 * 24 * 365 * 2),
- ));
- loop {
- let timeout = if let Some(timeout) = &mut self.timeout {
- timeout
- } else {
- &mut forever
- };
- tokio::select! {
- sock = listener.accept() => {
+ pub enum Event {
+ Request(std::io::Result<tokio::net::UnixStream>),
+ Timeout(()),
+ Sync(()),
+ }
+
+ let notifications = self
+ .state
+ .lock()
+ .await
+ .notifications_handler
+ .get_channel()
+ .await;
+ let notifications =
+ tokio_stream::wrappers::UnboundedReceiverStream::new(
+ notifications,
+ )
+ .map(|message| match message {
+ notifications::Message::Logout => Event::Timeout(()),
+ notifications::Message::Sync => Event::Sync(()),
+ })
+ .boxed();
+
+ let mut stream = futures_util::stream::select_all([
+ tokio_stream::wrappers::UnixListenerStream::new(listener)
+ .map(Event::Request)
+ .boxed(),
+ tokio_stream::wrappers::UnboundedReceiverStream::new(
+ self.timer_r,
+ )
+ .map(Event::Timeout)
+ .boxed(),
+ tokio_stream::wrappers::UnboundedReceiverStream::new(
+ self.sync_timer_r,
+ )
+ .map(Event::Sync)
+ .boxed(),
+ notifications,
+ ]);
+ while let Some(event) = stream.next().await {
+ match event {
+ Event::Request(res) => {
let mut sock = crate::sock::Sock::new(
- sock.context("failed to accept incoming connection")?.0
+ res.context("failed to accept incoming connection")?,
);
let state = self.state.clone();
tokio::spawn(async move {
- let res
- = handle_request(&mut sock, state.clone()).await;
+ let res =
+ handle_request(&mut sock, state.clone()).await;
if let Err(e) = res {
// unwrap is the only option here
sock.send(&rbw::protocol::Response::Error {
- error: format!("{:#}", e),
- }).await.unwrap();
+ error: format!("{e:#}"),
+ })
+ .await
+ .unwrap();
}
});
}
- _ = timeout => {
+ Event::Timeout(()) => {
+ self.state.lock().await.clear();
+ }
+ Event::Sync(()) => {
let state = self.state.clone();
- tokio::spawn(async move{
- state.write().await.clear();
+ tokio::spawn(async move {
+ // this could fail if we aren't logged in, but we
+ // don't care about that
+ if let Err(e) =
+ crate::actions::sync(None, state.clone()).await
+ {
+ eprintln!("failed to sync: {e:#}");
+ }
});
- }
- Some(ev) = self.timeout_chan.recv() => {
- match ev {
- TimeoutEvent::Set => self.set_timeout(),
- TimeoutEvent::Clear => self.clear_timeout(),
- }
+ self.state.lock().await.set_sync_timeout();
}
}
}
+ Ok(())
}
}
async fn handle_request(
sock: &mut crate::sock::Sock,
- state: std::sync::Arc<tokio::sync::RwLock<State>>,
+ state: std::sync::Arc<tokio::sync::Mutex<State>>,
) -> anyhow::Result<()> {
let req = sock.recv().await?;
let req = match req {
@@ -148,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 => {
@@ -161,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 {
@@ -187,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?;
@@ -195,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 69411ae..d470e10 100644
--- a/src/bin/rbw-agent/main.rs
+++ b/src/bin/rbw-agent/main.rs
@@ -1,4 +1,19 @@
+#![warn(clippy::cargo)]
+#![warn(clippy::pedantic)]
+#![warn(clippy::nursery)]
+#![warn(clippy::as_conversions)]
+#![warn(clippy::get_unwrap)]
+#![allow(clippy::cognitive_complexity)]
+#![allow(clippy::missing_const_for_fn)]
+#![allow(clippy::similar_names)]
+#![allow(clippy::struct_excessive_bools)]
#![allow(clippy::too_many_arguments)]
+#![allow(clippy::too_many_lines)]
+#![allow(clippy::type_complexity)]
+#![allow(clippy::multiple_crate_versions)]
+#![allow(clippy::large_enum_variant)]
+// this one looks plausibly useful, but currently has too many bugs
+#![allow(clippy::significant_drop_tightening)]
use anyhow::Context as _;
@@ -6,7 +21,9 @@ mod actions;
mod agent;
mod daemon;
mod debugger;
+mod notifications;
mod sock;
+mod timeout;
async fn tokio_main(
startup_ack: Option<crate::daemon::StartupAck>,
@@ -17,7 +34,7 @@ async fn tokio_main(
startup_ack.ack()?;
}
- let mut agent = crate::agent::Agent::new()?;
+ let agent = crate::agent::Agent::new()?;
agent.run(listener).await?;
Ok(())
@@ -29,11 +46,11 @@ fn real_main() -> anyhow::Result<()> {
)
.init();
- let no_daemonize = if let Some(arg) = std::env::args().nth(1) {
- arg == "--no-daemonize"
- } else {
- false
- };
+ let no_daemonize = std::env::args()
+ .nth(1)
+ .map_or(false, |arg| arg == "--no-daemonize");
+
+ rbw::dirs::make_all()?;
let startup_ack = if no_daemonize {
None
@@ -69,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 311176c..280b8cc 100644
--- a/src/bin/rbw-agent/sock.rs
+++ b/src/bin/rbw-agent/sock.rs
@@ -36,9 +36,8 @@ impl Sock {
buf.read_line(&mut line)
.await
.context("failed to read message from socket")?;
- Ok(serde_json::from_str(&line).map_err(|e| {
- format!("failed to parse message '{}': {}", line, e)
- }))
+ Ok(serde_json::from_str(&line)
+ .map_err(|e| format!("failed to parse message '{line}': {e}")))
}
}
diff --git a/src/bin/rbw-agent/timeout.rs b/src/bin/rbw-agent/timeout.rs
new file mode 100644
index 0000000..e2aba06
--- /dev/null
+++ b/src/bin/rbw-agent/timeout.rs
@@ -0,0 +1,66 @@
+use futures_util::StreamExt as _;
+
+#[derive(Debug, Hash, Eq, PartialEq, Copy, Clone)]
+enum Streams {
+ Requests,
+ Timer,
+}
+
+#[derive(Debug)]
+enum Action {
+ Set(std::time::Duration),
+ Clear,
+}
+
+pub struct Timeout {
+ req_w: tokio::sync::mpsc::UnboundedSender<Action>,
+}
+
+impl Timeout {
+ pub fn new() -> (Self, tokio::sync::mpsc::UnboundedReceiver<()>) {
+ let (req_w, req_r) = tokio::sync::mpsc::unbounded_channel();
+ let (timer_w, timer_r) = tokio::sync::mpsc::unbounded_channel();
+ tokio::spawn(async move {
+ enum Event {
+ Request(Action),
+ Timer,
+ }
+ let mut stream = tokio_stream::StreamMap::new();
+ stream.insert(
+ Streams::Requests,
+ tokio_stream::wrappers::UnboundedReceiverStream::new(req_r)
+ .map(Event::Request)
+ .boxed(),
+ );
+ while let Some(event) = stream.next().await {
+ match event {
+ (_, Event::Request(Action::Set(dur))) => {
+ stream.insert(
+ Streams::Timer,
+ futures_util::stream::once(tokio::time::sleep(
+ dur,
+ ))
+ .map(|()| Event::Timer)
+ .boxed(),
+ );
+ }
+ (_, Event::Request(Action::Clear)) => {
+ stream.remove(&Streams::Timer);
+ }
+ (_, Event::Timer) => {
+ timer_w.send(()).unwrap();
+ }
+ }
+ }
+ });
+ (Self { req_w }, timer_r)
+ }
+
+ pub fn set(&self, dur: std::time::Duration) {
+ self.req_w.send(Action::Set(dur)).unwrap();
+ }
+
+ pub fn clear(&self) {
+ self.req_w.send(Action::Clear).unwrap();
+ }
+}
diff --git a/src/bin/rbw/actions.rs b/src/bin/rbw/actions.rs
index 39fde15..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(|s| s.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(|s| s.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(|s| s.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(|s| s.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(|s| s.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 9efd966..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,25 +58,25 @@ struct DecryptedCipher {
}
impl DecryptedCipher {
- fn display_short(&self, desc: &str) -> bool {
+ fn display_short(&self, desc: &str, clipboard: bool) -> bool {
match &self.data {
DecryptedData::Login { password, .. } => {
- if let Some(password) = password {
- println!("{}", password);
- true
- } else {
- eprintln!("entry for '{}' had no password", desc);
- false
- }
+ password.as_ref().map_or_else(
+ || {
+ eprintln!("entry for '{desc}' had no password");
+ false
+ },
+ |password| val_display_or_store(clipboard, password),
+ )
}
DecryptedData::Card { number, .. } => {
- if let Some(number) = number {
- println!("{}", number);
- true
- } else {
- eprintln!("entry for '{}' had no card number", desc);
- false
- }
+ number.as_ref().map_or_else(
+ || {
+ eprintln!("entry for '{desc}' had no card number");
+ false
+ },
+ |number| val_display_or_store(clipboard, number),
+ )
}
DecryptedData::Identity {
title,
@@ -54,30 +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 {} => {
- if let Some(notes) = &self.notes {
- println!("{}", notes);
- true
- } else {
- eprintln!("entry for '{}' had no notes", desc);
+ DecryptedData::SecureNote {} => self.notes.as_ref().map_or_else(
+ || {
+ eprintln!("entry for '{desc}' had no notes");
false
+ },
+ |notes| val_display_or_store(clipboard, notes),
+ ),
+ }
+ }
+
+ fn display_field(&self, desc: &str, field: &str, clipboard: bool) {
+ let field = field.to_lowercase();
+ let field = field.as_str();
+ match &self.data {
+ DecryptedData::Login {
+ username,
+ totp,
+ uris,
+ ..
+ } => match field {
+ "notes" => {
+ if let Some(notes) = &self.notes {
+ val_display_or_store(clipboard, notes);
+ }
}
- }
+ "username" | "user" => {
+ if let Some(username) = &username {
+ val_display_or_store(clipboard, username);
+ }
+ }
+ "totp" | "code" => {
+ if let Some(totp) = totp {
+ match generate_totp(totp) {
+ Ok(code) => {
+ val_display_or_store(clipboard, &code);
+ }
+ Err(e) => {
+ eprintln!("{e}");
+ }
+ }
+ }
+ }
+ "uris" | "urls" | "sites" => {
+ if let Some(uris) = uris {
+ let uri_strs: Vec<_> = uris
+ .iter()
+ .map(|uri| uri.uri.to_string())
+ .collect();
+ val_display_or_store(clipboard, &uri_strs.join("\n"));
+ }
+ }
+ "password" => {
+ self.display_short(desc, clipboard);
+ }
+ _ => {
+ for f in &self.fields {
+ if let Some(name) = &f.name {
+ if name.to_lowercase().as_str().contains(field) {
+ val_display_or_store(
+ clipboard,
+ f.value.as_deref().unwrap_or(""),
+ );
+ break;
+ }
+ }
+ }
+ }
+ },
+ DecryptedData::Card {
+ cardholder_name,
+ brand,
+ exp_month,
+ exp_year,
+ code,
+ ..
+ } => match field {
+ "number" | "card" => {
+ self.display_short(desc, clipboard);
+ }
+ "exp" => {
+ if let (Some(month), Some(year)) = (exp_month, exp_year) {
+ val_display_or_store(
+ clipboard,
+ &format!("{month}/{year}"),
+ );
+ }
+ }
+ "exp_month" | "month" => {
+ if let Some(exp_month) = exp_month {
+ val_display_or_store(clipboard, exp_month);
+ }
+ }
+ "exp_year" | "year" => {
+ if let Some(exp_year) = exp_year {
+ val_display_or_store(clipboard, exp_year);
+ }
+ }
+ "cvv" => {
+ if let Some(code) = code {
+ val_display_or_store(clipboard, code);
+ }
+ }
+ "name" | "cardholder" => {
+ if let Some(cardholder_name) = cardholder_name {
+ val_display_or_store(clipboard, cardholder_name);
+ }
+ }
+ "brand" | "type" => {
+ if let Some(brand) = brand {
+ val_display_or_store(clipboard, brand);
+ }
+ }
+ "notes" => {
+ if let Some(notes) = &self.notes {
+ val_display_or_store(clipboard, notes);
+ }
+ }
+ _ => {
+ for f in &self.fields {
+ if let Some(name) = &f.name {
+ if name.to_lowercase().as_str().contains(field) {
+ val_display_or_store(
+ clipboard,
+ f.value.as_deref().unwrap_or(""),
+ );
+ break;
+ }
+ }
+ }
+ }
+ },
+ DecryptedData::Identity {
+ address1,
+ address2,
+ address3,
+ city,
+ state,
+ postal_code,
+ country,
+ phone,
+ email,
+ ssn,
+ license_number,
+ passport_number,
+ username,
+ ..
+ } => match field {
+ "name" => {
+ self.display_short(desc, clipboard);
+ }
+ "email" => {
+ if let Some(email) = email {
+ val_display_or_store(clipboard, email);
+ }
+ }
+ "address" => {
+ let mut strs = vec![];
+ if let Some(address1) = address1 {
+ strs.push(address1.clone());
+ }
+ if let Some(address2) = address2 {
+ strs.push(address2.clone());
+ }
+ if let Some(address3) = address3 {
+ strs.push(address3.clone());
+ }
+ if !strs.is_empty() {
+ val_display_or_store(clipboard, &strs.join("\n"));
+ }
+ }
+ "city" => {
+ if let Some(city) = city {
+ val_display_or_store(clipboard, city);
+ }
+ }
+ "state" => {
+ if let Some(state) = state {
+ val_display_or_store(clipboard, state);
+ }
+ }
+ "postcode" | "zipcode" | "zip" => {
+ if let Some(postal_code) = postal_code {
+ val_display_or_store(clipboard, postal_code);
+ }
+ }
+ "country" => {
+ if let Some(country) = country {
+ val_display_or_store(clipboard, country);
+ }
+ }
+ "phone" => {
+ if let Some(phone) = phone {
+ val_display_or_store(clipboard, phone);
+ }
+ }
+ "ssn" => {
+ if let Some(ssn) = ssn {
+ val_display_or_store(clipboard, ssn);
+ }
+ }
+ "license" => {
+ if let Some(license_number) = license_number {
+ val_display_or_store(clipboard, license_number);
+ }
+ }
+ "passport" => {
+ if let Some(passport_number) = passport_number {
+ val_display_or_store(clipboard, passport_number);
+ }
+ }
+ "username" => {
+ if let Some(username) = username {
+ val_display_or_store(clipboard, username);
+ }
+ }
+ "notes" => {
+ if let Some(notes) = &self.notes {
+ val_display_or_store(clipboard, notes);
+ }
+ }
+ _ => {
+ for f in &self.fields {
+ if let Some(name) = &f.name {
+ if name.to_lowercase().as_str().contains(field) {
+ val_display_or_store(
+ clipboard,
+ f.value.as_deref().unwrap_or(""),
+ );
+ break;
+ }
+ }
+ }
+ }
+ },
+ DecryptedData::SecureNote {} => match field {
+ "note" | "notes" => {
+ self.display_short(desc, clipboard);
+ }
+ _ => {
+ for f in &self.fields {
+ if let Some(name) = &f.name {
+ if name.to_lowercase().as_str().contains(field) {
+ val_display_or_store(
+ clipboard,
+ f.value.as_deref().unwrap_or(""),
+ );
+ break;
+ }
+ }
+ }
+ }
+ },
}
}
- fn display_long(&self, desc: &str) {
+ fn display_long(&self, desc: &str, clipboard: bool) {
match &self.data {
DecryptedData::Login {
username,
@@ -85,29 +362,31 @@ impl DecryptedCipher {
uris,
..
} => {
- let mut displayed = self.display_short(desc);
+ let mut displayed = self.display_short(desc, clipboard);
displayed |=
- self.display_field("Username", username.as_deref());
+ display_field("Username", username.as_deref(), clipboard);
displayed |=
- self.display_field("TOTP Secret", totp.as_deref());
+ display_field("TOTP Secret", totp.as_deref(), clipboard);
if let Some(uris) = uris {
for uri in uris {
displayed |=
- self.display_field("URI", Some(&uri.uri));
+ display_field("URI", Some(&uri.uri), clipboard);
let match_type =
- uri.match_type.map(|ty| format!("{}", ty));
- displayed |= self.display_field(
+ uri.match_type.map(|ty| format!("{ty}"));
+ displayed |= display_field(
"Match type",
match_type.as_deref(),
+ clipboard,
);
}
}
for field in &self.fields {
- displayed |= self.display_field(
+ displayed |= display_field(
field.name.as_deref().unwrap_or("(null)"),
Some(field.value.as_deref().unwrap_or("")),
+ clipboard,
);
}
@@ -115,7 +394,7 @@ impl DecryptedCipher {
if displayed {
println!();
}
- println!("{}", notes);
+ println!("{notes}");
}
}
DecryptedData::Card {
@@ -126,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 |= self.display_field("CVV", code.as_deref());
+ displayed |= display_field("CVV", code.as_deref(), clipboard);
+ displayed |= display_field(
+ "Name",
+ cardholder_name.as_deref(),
+ clipboard,
+ );
displayed |=
- self.display_field("Name", cardholder_name.as_deref());
- displayed |= self.display_field("Brand", brand.as_deref());
+ display_field("Brand", brand.as_deref(), clipboard);
if let Some(notes) = &self.notes {
if displayed {
println!();
}
- println!("{}", notes);
+ println!("{notes}");
}
}
DecryptedData::Identity {
@@ -162,74 +445,110 @@ impl DecryptedCipher {
username,
..
} => {
- let mut displayed = self.display_short(desc);
+ let mut displayed = self.display_short(desc, clipboard);
displayed |=
- self.display_field("Address", address1.as_deref());
+ display_field("Address", address1.as_deref(), clipboard);
displayed |=
- self.display_field("Address", address2.as_deref());
+ display_field("Address", address2.as_deref(), clipboard);
displayed |=
- self.display_field("Address", address3.as_deref());
- displayed |= self.display_field("City", city.as_deref());
- displayed |= self.display_field("State", state.as_deref());
+ display_field("Address", address3.as_deref(), clipboard);
displayed |=
- self.display_field("Postcode", postal_code.as_deref());
+ display_field("City", city.as_deref(), clipboard);
displayed |=
- self.display_field("Country", country.as_deref());
- displayed |= self.display_field("Phone", phone.as_deref());
- displayed |= self.display_field("Email", email.as_deref());
- displayed |= self.display_field("SSN", ssn.as_deref());
+ display_field("State", state.as_deref(), clipboard);
+ displayed |= display_field(
+ "Postcode",
+ postal_code.as_deref(),
+ clipboard,
+ );
displayed |=
- self.display_field("License", license_number.as_deref());
- displayed |= self
- .display_field("Passport", passport_number.as_deref());
+ display_field("Country", country.as_deref(), clipboard);
displayed |=
- self.display_field("Username", username.as_deref());
+ 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);
}
}
}
- fn display_field(&self, name: &str, field: Option<&str>) -> bool {
- if let Some(field) = field {
- println!("{}: {}", name, field);
- true
- } else {
- false
- }
- }
-
fn display_name(&self) -> String {
match &self.data {
DecryptedData::Login { username, .. } => {
- if let Some(username) = username {
- format!("{}@{}", username, self.name)
- } else {
- self.name.clone()
- }
+ username.as_ref().map_or_else(
+ || self.name.clone(),
+ |username| format!("{}@{}", username, self.name),
+ )
}
_ => self.name.clone(),
}
}
+ fn display_json(&self, desc: &str) -> anyhow::Result<()> {
+ serde_json::to_writer_pretty(std::io::stdout(), &self)
+ .context(format!("failed to write entry '{desc}' to stdout"))?;
+ println!();
+
+ Ok(())
+ }
+
fn exact_match(
&self,
- name: &str,
+ 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 {
@@ -312,7 +631,23 @@ impl DecryptedCipher {
}
}
-#[derive(Debug, Clone)]
+fn val_display_or_store(clipboard: bool, password: &str) -> bool {
+ if clipboard {
+ match clipboard_store(password) {
+ Ok(()) => true,
+ Err(e) => {
+ eprintln!("{e}");
+ false
+ }
+ }
+ } else {
+ println!("{password}");
+ true
+ }
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(untagged)]
#[cfg_attr(test, derive(Eq, PartialEq))]
enum DecryptedData {
Login {
@@ -351,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,
@@ -393,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()?;
@@ -415,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()
@@ -425,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)),
}
@@ -447,8 +870,10 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> {
"email" => config.email = None,
"base_url" => config.base_url = None,
"identity_url" => config.identity_url = None,
+ "notifications_url" => config.notifications_url = None,
+ "client_cert_path" => config.client_cert_path = None,
"lock_timeout" => {
- config.lock_timeout = rbw::config::default_lock_timeout()
+ config.lock_timeout = rbw::config::default_lock_timeout();
}
"pinentry" => config.pinentry = rbw::config::default_pinentry(),
_ => return Err(anyhow::anyhow!("invalid config key: {}", key)),
@@ -465,6 +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()?;
@@ -514,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));
@@ -526,30 +957,40 @@ pub fn list(fields: &[String]) -> anyhow::Result<()> {
ListField::Name => cipher.name.clone(),
ListField::Id => cipher.id.clone(),
ListField::User => match &cipher.data {
- DecryptedData::Login { username, .. } => username
- .as_ref()
- .map(std::string::ToString::to_string)
- .unwrap_or_else(|| "".to_string()),
- _ => "".to_string(),
+ DecryptedData::Login { username, .. } => {
+ username.as_ref().map_or_else(
+ String::new,
+ std::string::ToString::to_string,
+ )
+ }
+ _ => String::new(),
},
- ListField::Folder => cipher
- .folder
- .as_ref()
- .map(std::string::ToString::to_string)
- .unwrap_or_else(|| "".to_string()),
+ ListField::Folder => cipher.folder.as_ref().map_or_else(
+ String::new,
+ std::string::ToString::to_string,
+ ),
})
.collect();
- println!("{}", values.join("\t"));
+
+ // write to stdout but don't panic when pipe get's closed
+ // this happens when piping stdout in a shell
+ match writeln!(&mut io::stdout(), "{}", values.join("\t")) {
+ Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
+ res => res,
+ }?;
}
Ok(())
}
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()?;
@@ -557,17 +998,20 @@ pub fn get(
let desc = format!(
"{}{}",
- user.map(|s| format!("{}@", s))
- .unwrap_or_else(|| "".to_string()),
- 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(())
@@ -577,6 +1021,7 @@ pub fn code(
name: &str,
user: Option<&str>,
folder: Option<&str>,
+ clipboard: bool,
) -> anyhow::Result<()> {
unlock()?;
@@ -584,17 +1029,17 @@ pub fn code(
let desc = format!(
"{}{}",
- user.map(|s| format!("{}@", s))
- .unwrap_or_else(|| "".to_string()),
+ user.map_or_else(String::new, |s| format!("{s}@")),
name
);
- let (_, decrypted) = find_entry(&db, name, user, folder)
- .with_context(|| format!("couldn't find entry for '{}'", desc))?;
+ 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"
@@ -610,7 +1055,7 @@ pub fn code(
pub fn add(
name: &str,
username: Option<&str>,
- uris: Vec<(String, Option<rbw::api::UriMatchType>)>,
+ uris: &[(String, Option<rbw::api::UriMatchType>)],
folder: Option<&str>,
) -> anyhow::Result<()> {
unlock()?;
@@ -627,7 +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
@@ -707,13 +1152,13 @@ pub fn add(
pub fn generate(
name: Option<&str>,
username: Option<&str>,
- uris: Vec<(String, Option<rbw::api::UriMatchType>)>,
+ uris: &[(String, Option<rbw::api::UriMatchType>)],
folder: Option<&str>,
len: usize,
ty: rbw::pwgen::Type,
) -> anyhow::Result<()> {
let password = rbw::pwgen::pwgen(ty, len);
- println!("{}", password);
+ println!("{password}");
if let Some(name) = name {
unlock()?;
@@ -813,24 +1258,23 @@ pub fn edit(
let desc = format!(
"{}{}",
- username
- .map(|s| format!("{}@", s))
- .unwrap_or_else(|| "".to_string()),
+ username.map_or_else(String::new, |s| format!("{s}@")),
name
);
- let (entry, decrypted) = find_entry(&db, name, username, folder)
- .with_context(|| format!("couldn't find entry for '{}'", desc))?;
+ 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
@@ -847,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 {
@@ -874,14 +1317,34 @@ pub fn edit(
let data = rbw::db::EntryData::Login {
username: entry_username.clone(),
password,
- uris: entry_uris.to_vec(),
+ uris: entry_uris.clone(),
totp: entry_totp.clone(),
};
- (data, notes, history)
+ (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"
));
}
};
@@ -893,6 +1356,7 @@ pub fn edit(
entry.org_id.as_deref(),
&entry.name,
&data,
+ &fields,
notes.as_deref(),
entry.folder_id.as_deref(),
&history,
@@ -918,14 +1382,13 @@ pub fn remove(
let desc = format!(
"{}{}",
- username
- .map(|s| format!("{}@", s))
- .unwrap_or_else(|| "".to_string()),
+ username.map_or_else(String::new, |s| format!("{s}@")),
name
);
- let (entry, _) = find_entry(&db, name, username, folder)
- .with_context(|| format!("couldn't find entry for '{}'", desc))?;
+ 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)?
@@ -950,14 +1413,13 @@ pub fn history(
let desc = format!(
"{}{}",
- username
- .map(|s| format!("{}@", s))
- .unwrap_or_else(|| "".to_string()),
+ username.map_or_else(String::new, |s| format!("{s}@")),
name
);
- let (_, decrypted) = find_entry(&db, name, username, folder)
- .with_context(|| format!("couldn't find entry for '{}'", desc))?;
+ 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);
}
@@ -1017,7 +1479,7 @@ fn ensure_agent_once() -> anyhow::Result<()> {
let agent_path = std::env::var("RBW_AGENT");
let agent_path = agent_path
.as_ref()
- .map(|s| s.as_str())
+ .map(std::string::String::as_str)
.unwrap_or("rbw-agent");
let status = std::process::Command::new(agent_path)
.status()
@@ -1052,45 +1514,42 @@ 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)> {
- match uuid::Uuid::parse_str(name) {
- Ok(_) => {
- for cipher in &db.entries {
- if name == cipher.id {
- return Ok((cipher.clone(), decrypt_cipher(cipher)?));
- }
+ if let Needle::Uuid(uuid) = needle {
+ for cipher in &db.entries {
+ if uuid::Uuid::parse_str(&cipher.id) == Ok(*uuid) {
+ return Ok((cipher.clone(), decrypt_cipher(cipher)?));
}
- Err(anyhow::anyhow!("no entry found"))
- }
- Err(_) => {
- let ciphers: Vec<(rbw::db::Entry, DecryptedCipher)> = db
- .entries
- .iter()
- .cloned()
- .map(|entry| {
- decrypt_cipher(&entry).map(|decrypted| (entry, decrypted))
- })
- .collect::<anyhow::Result<_>>()?;
- find_entry_raw(&ciphers, name, username, folder)
}
+ Err(anyhow::anyhow!("no entry found"))
+ } else {
+ let ciphers: Vec<(rbw::db::Entry, DecryptedCipher)> = db
+ .entries
+ .iter()
+ .cloned()
+ .map(|entry| {
+ decrypt_cipher(&entry).map(|decrypted| (entry, decrypted))
+ })
+ .collect::<anyhow::Result<_>>()?;
+ find_entry_raw(&ciphers, 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 {
@@ -1100,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 {
@@ -1111,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() {
@@ -1435,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();
}
@@ -1447,32 +1912,35 @@ fn parse_editor(contents: &str) -> (Option<String>, Option<String>) {
fn load_db() -> anyhow::Result<rbw::db::Db> {
let config = rbw::config::Config::load()?;
- if let Some(email) = &config.email {
- rbw::db::Db::load(&config.server_name(), email)
- .map_err(anyhow::Error::new)
- } else {
- Err(anyhow::anyhow!("failed to find email address in config"))
- }
+ config.email.as_ref().map_or_else(
+ || Err(anyhow::anyhow!("failed to find email address in config")),
+ |email| {
+ rbw::db::Db::load(&config.server_name(), email)
+ .map_err(anyhow::Error::new)
+ },
+ )
}
fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> {
let config = rbw::config::Config::load()?;
- if let Some(email) = &config.email {
- db.save(&config.server_name(), email)
- .map_err(anyhow::Error::new)
- } else {
- Err(anyhow::anyhow!("failed to find email address in config"))
- }
+ config.email.as_ref().map_or_else(
+ || Err(anyhow::anyhow!("failed to find email address in config")),
+ |email| {
+ db.save(&config.server_name(), email)
+ .map_err(anyhow::Error::new)
+ },
+ )
}
fn remove_db() -> anyhow::Result<()> {
let config = rbw::config::Config::load()?;
- if let Some(email) = &config.email {
- rbw::db::Db::remove(&config.server_name(), email)
- .map_err(anyhow::Error::new)
- } else {
- Err(anyhow::anyhow!("failed to find email address in config"))
- }
+ config.email.as_ref().map_or_else(
+ || Err(anyhow::anyhow!("failed to find email address in config")),
+ |email| {
+ rbw::db::Db::remove(&config.server_name(), email)
+ .map_err(anyhow::Error::new)
+ },
+ )
}
fn parse_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> {
@@ -1500,7 +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"))
}
@@ -1517,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::*;
@@ -1524,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!(
@@ -1591,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),
@@ -1643,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,
@@ -1656,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![],
@@ -1664,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,
diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs
index 85631c5..eefa52a 100644
--- a/src/bin/rbw/main.rs
+++ b/src/bin/rbw/main.rs
@@ -1,23 +1,36 @@
+#![warn(clippy::cargo)]
+#![warn(clippy::pedantic)]
+#![warn(clippy::nursery)]
+#![warn(clippy::as_conversions)]
+#![warn(clippy::get_unwrap)]
+#![allow(clippy::cognitive_complexity)]
+#![allow(clippy::missing_const_for_fn)]
+#![allow(clippy::similar_names)]
+#![allow(clippy::struct_excessive_bools)]
+#![allow(clippy::too_many_arguments)]
+#![allow(clippy::too_many_lines)]
+#![allow(clippy::type_complexity)]
+#![allow(clippy::multiple_crate_versions)]
#![allow(clippy::large_enum_variant)]
use anyhow::Context as _;
+use clap::{CommandFactory as _, Parser as _};
use std::io::Write as _;
-use structopt::StructOpt as _;
mod actions;
mod commands;
mod sock;
-#[derive(Debug, structopt::StructOpt)]
-#[structopt(about = "Unofficial Bitwarden CLI")]
+#[derive(Debug, clap::Parser)]
+#[command(version, about = "Unofficial Bitwarden CLI")]
enum Opt {
- #[structopt(about = "Get or set configuration options")]
+ #[command(about = "Get or set configuration options")]
Config {
- #[structopt(subcommand)]
+ #[command(subcommand)]
config: Config,
},
- #[structopt(
+ #[command(
about = "Register this device with the Bitwarden server",
long_about = "Register this device with the Bitwarden server\n\n\
The official Bitwarden server includes bot detection to prevent \
@@ -28,60 +41,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 \
@@ -91,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",
@@ -120,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 \
@@ -162,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 \
@@ -173,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 {
@@ -246,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,
},
}
@@ -275,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())
}
@@ -303,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,
@@ -319,7 +358,7 @@ fn main(opt: Opt) {
} => commands::add(
name,
user.as_deref(),
- uri.iter()
+ &uri.iter()
// XXX not sure what the ui for specifying the match type
// should be
.map(|uri| (uri.clone(), None))
@@ -351,7 +390,7 @@ fn main(opt: Opt) {
commands::generate(
name.as_deref(),
user.as_deref(),
- uri.iter()
+ &uri.iter()
// XXX not sure what the ui for specifying the match type
// should be
.map(|uri| (uri.clone(), None))
@@ -373,25 +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 bbc39f7..efb1b5f 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,51 +1,59 @@
use crate::prelude::*;
use std::io::{Read as _, Write as _};
-use tokio::io::AsyncReadExt as _;
+use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Config {
pub email: Option<String>,
pub base_url: Option<String>,
pub identity_url: Option<String>,
+ pub notifications_url: Option<String>,
#[serde(default = "default_lock_timeout")]
pub lock_timeout: u64,
+ #[serde(default = "default_sync_interval")]
+ pub sync_interval: u64,
#[serde(default = "default_pinentry")]
pub pinentry: String,
- #[serde(default = "stub_device_id")]
- pub device_id: String,
+ pub client_cert_path: Option<std::path::PathBuf>,
+ // backcompat, no longer generated in new configs
+ #[serde(skip_serializing)]
+ pub device_id: Option<String>,
}
impl Default for Config {
fn default() -> Self {
Self {
- email: Default::default(),
- base_url: Default::default(),
- identity_url: Default::default(),
+ email: None,
+ base_url: None,
+ identity_url: None,
+ notifications_url: None,
lock_timeout: default_lock_timeout(),
+ sync_interval: default_sync_interval(),
pinentry: default_pinentry(),
- device_id: default_device_id(),
+ client_cert_path: None,
+ device_id: None,
}
}
}
+#[must_use]
pub fn default_lock_timeout() -> u64 {
3600
}
-pub fn default_pinentry() -> String {
- "pinentry".to_string()
-}
-
-fn default_device_id() -> String {
- uuid::Uuid::new_v4().to_hyphenated().to_string()
+#[must_use]
+pub fn default_sync_interval() -> u64 {
+ 3600
}
-fn stub_device_id() -> String {
- String::from("fix")
+#[must_use]
+pub fn default_pinentry() -> String {
+ "pinentry".to_string()
}
impl Config {
+ #[must_use]
pub fn new() -> Self {
Self::default()
}
@@ -127,36 +135,103 @@ impl Config {
}
pub fn validate() -> Result<()> {
- let mut config = Self::load()?;
+ let config = Self::load()?;
if config.email.is_none() {
return Err(Error::ConfigMissingEmail);
}
- if config.device_id == stub_device_id() {
- config.device_id = default_device_id();
- config.save()?;
- }
Ok(())
}
+ #[must_use]
pub fn base_url(&self) -> String {
self.base_url.clone().map_or_else(
|| "https://api.bitwarden.com".to_string(),
- |url| format!("{}/api", url.trim_end_matches('/')),
+ |url| {
+ let clean_url = url.trim_end_matches('/').to_string();
+ if clean_url == "https://api.bitwarden.eu" {
+ clean_url
+ } else {
+ format!("{clean_url}/api")
+ }
+ },
)
}
+ #[must_use]
pub fn identity_url(&self) -> String {
self.identity_url.clone().unwrap_or_else(|| {
self.base_url.clone().map_or_else(
|| "https://identity.bitwarden.com".to_string(),
- |url| format!("{}/identity", url.trim_end_matches('/')),
+ |url| {
+ let clean_url = url.trim_end_matches('/').to_string();
+ if clean_url == "https://identity.bitwarden.eu" {
+ clean_url
+ } else {
+ format!("{clean_url}/identity")
+ }
+ },
+ )
+ })
+ }
+
+ #[must_use]
+ pub fn notifications_url(&self) -> String {
+ self.notifications_url.clone().unwrap_or_else(|| {
+ self.base_url.clone().map_or_else(
+ || "https://notifications.bitwarden.com".to_string(),
+ |url| {
+ let clean_url = url.trim_end_matches('/').to_string();
+ if clean_url == "https://notifications.bitwarden.eu" {
+ clean_url
+ } else {
+ format!("{clean_url}/notifications")
+ }
+ },
)
})
}
+ #[must_use]
+ pub fn client_cert_path(&self) -> Option<&std::path::Path> {
+ self.client_cert_path.as_deref()
+ }
+
+ #[must_use]
pub fn server_name(&self) -> String {
self.base_url
.clone()
.unwrap_or_else(|| "default".to_string())
}
}
+
+pub async fn device_id(config: &Config) -> Result<String> {
+ let file = crate::dirs::device_id_file();
+ if let Ok(mut fh) = tokio::fs::File::open(&file).await {
+ let mut s = String::new();
+ fh.read_to_string(&mut s)
+ .await
+ .map_err(|e| Error::LoadDeviceId {
+ source: e,
+ file: file.clone(),
+ })?;
+ Ok(s.trim().to_string())
+ } else {
+ let id = config.device_id.as_ref().map_or_else(
+ || uuid::Uuid::new_v4().hyphenated().to_string(),
+ String::to_string,
+ );
+ let mut fh = tokio::fs::File::create(&file).await.map_err(|e| {
+ Error::LoadDeviceId {
+ source: e,
+ file: file.clone(),
+ }
+ })?;
+ fh.write_all(id.as_bytes()).await.map_err(|e| {
+ Error::LoadDeviceId {
+ source: e,
+ file: file.clone(),
+ }
+ })?;
+ Ok(id)
+ }
+}
diff --git a/src/db.rs b/src/db.rs
index 5359321..ab742ea 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -147,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(
@@ -164,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>,
@@ -173,6 +178,7 @@ pub struct Db {
}
impl Db {
+ #[must_use]
pub fn new() -> Self {
Self::default()
}
@@ -287,10 +293,12 @@ impl Db {
Ok(())
}
+ #[must_use]
pub fn needs_login(&self) -> bool {
self.access_token.is_none()
|| self.refresh_token.is_none()
|| self.iterations.is_none()
+ || self.kdf.is_none()
|| self.protected_key.is_none()
}
}
diff --git a/src/dirs.rs b/src/dirs.rs
index 285a0d5..2fa6e50 100644
--- a/src/dirs.rs
+++ b/src/dirs.rs
@@ -37,59 +37,89 @@ pub fn make_all() -> Result<()> {
Ok(())
}
+#[must_use]
pub fn config_file() -> std::path::PathBuf {
config_dir().join("config.json")
}
const INVALID_PATH: &percent_encoding::AsciiSet =
&percent_encoding::CONTROLS.add(b'/').add(b'%').add(b':');
+#[must_use]
pub fn db_file(server: &str, email: &str) -> std::path::PathBuf {
let server =
percent_encoding::percent_encode(server.as_bytes(), INVALID_PATH)
.to_string();
- cache_dir().join(format!("{}:{}.json", server, email))
+ cache_dir().join(format!("{server}:{email}.json"))
}
+#[must_use]
pub fn pid_file() -> std::path::PathBuf {
runtime_dir().join("pidfile")
}
+#[must_use]
pub fn agent_stdout_file() -> std::path::PathBuf {
data_dir().join("agent.out")
}
+#[must_use]
pub fn agent_stderr_file() -> std::path::PathBuf {
data_dir().join("agent.err")
}
+#[must_use]
+pub fn device_id_file() -> std::path::PathBuf {
+ data_dir().join("device_id")
+}
+
+#[must_use]
pub fn socket_file() -> std::path::PathBuf {
runtime_dir().join("socket")
}
+#[must_use]
fn config_dir() -> std::path::PathBuf {
- let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap();
+ let project_dirs =
+ directories::ProjectDirs::from("", "", &profile()).unwrap();
project_dirs.config_dir().to_path_buf()
}
+#[must_use]
fn cache_dir() -> std::path::PathBuf {
- let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap();
+ let project_dirs =
+ directories::ProjectDirs::from("", "", &profile()).unwrap();
project_dirs.cache_dir().to_path_buf()
}
+#[must_use]
fn data_dir() -> std::path::PathBuf {
- let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap();
+ let project_dirs =
+ directories::ProjectDirs::from("", "", &profile()).unwrap();
project_dirs.data_dir().to_path_buf()
}
+#[must_use]
fn runtime_dir() -> std::path::PathBuf {
- let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap();
- match project_dirs.runtime_dir() {
- Some(dir) => dir.to_path_buf(),
- None => format!(
- "{}/rbw-{}",
- std::env::temp_dir().to_string_lossy(),
- nix::unistd::getuid().as_raw()
- )
- .into(),
+ let project_dirs =
+ directories::ProjectDirs::from("", "", &profile()).unwrap();
+ project_dirs.runtime_dir().map_or_else(
+ || {
+ format!(
+ "{}/{}-{}",
+ std::env::temp_dir().to_string_lossy(),
+ &profile(),
+ 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 1a831a7..360f31f 100644
--- a/src/edit.rs
+++ b/src/edit.rs
@@ -2,7 +2,17 @@ use crate::prelude::*;
use std::io::{Read as _, Write as _};
+use is_terminal::IsTerminal as _;
+
pub fn edit(contents: &str, help: &str) -> Result<String> {
+ if !std::io::stdin().is_terminal() {
+ // directly read from piped content
+ return match std::io::read_to_string(std::io::stdin()) {
+ Err(e) => Err(Error::FailedToReadFromStdin { err: e }),
+ Ok(res) => Ok(res),
+ };
+ }
+
let mut var = "VISUAL";
let editor = std::env::var_os(var).unwrap_or_else(|| {
var = "EDITOR";
@@ -30,6 +40,7 @@ pub fn edit(contents: &str, help: &str) -> Result<String> {
let editor = std::path::Path::new(&editor);
let mut editor_args = vec![];
+ #[allow(clippy::single_match_else)] // more to come
match editor.file_name() {
Some(editor) => match editor.to_str() {
Some("vim" | "nvim") => {
@@ -52,7 +63,7 @@ pub fn edit(contents: &str, help: &str) -> Result<String> {
(editor, editor_args)
};
- let res = std::process::Command::new(&cmd).args(&args).status();
+ let res = std::process::Command::new(cmd).args(&args).status();
match res {
Ok(res) => {
if !res.success() {
@@ -80,8 +91,6 @@ pub fn edit(contents: &str, help: &str) -> Result<String> {
}
fn contains_shell_metacharacters(cmd: &std::ffi::OsStr) -> bool {
- match cmd.to_str() {
- Some(s) => s.contains(&[' ', '$', '\'', '"'][..]),
- None => false,
- }
+ cmd.to_str()
+ .map_or(false, |s| s.contains(&[' ', '$', '\'', '"'][..]))
}
diff --git a/src/error.rs b/src/error.rs
index 8116de2..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(),
@@ -116,6 +118,18 @@ pub enum Error {
file: std::path::PathBuf,
},
+ #[error("failed to load device id from {}", .file.display())]
+ LoadDeviceId {
+ source: tokio::io::Error,
+ file: std::path::PathBuf,
+ },
+
+ #[error("failed to load client cert from {}", .file.display())]
+ LoadClientCert {
+ source: tokio::io::Error,
+ file: std::path::PathBuf,
+ },
+
#[error("invalid padding")]
Padding,
@@ -125,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,
@@ -207,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 4a13e25..fd14fcf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,21 +1,24 @@
+#![warn(clippy::cargo)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
-#![allow(clippy::default_trait_access)]
-#![allow(clippy::implicit_hasher)]
-#![allow(clippy::large_enum_variant)]
+#![warn(clippy::as_conversions)]
+#![warn(clippy::get_unwrap)]
+#![allow(clippy::cognitive_complexity)]
#![allow(clippy::missing_const_for_fn)]
-#![allow(clippy::missing_errors_doc)]
-#![allow(clippy::missing_panics_doc)]
-#![allow(clippy::must_use_candidate)]
#![allow(clippy::similar_names)]
-#![allow(clippy::single_match)]
+#![allow(clippy::struct_excessive_bools)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::type_complexity)]
-#![allow(clippy::unused_async)]
+#![allow(clippy::multiple_crate_versions)]
+#![allow(clippy::large_enum_variant)]
+// we aren't really documenting apis anyway
+#![allow(clippy::missing_errors_doc)]
+#![allow(clippy::missing_panics_doc)]
pub mod actions;
pub mod api;
+pub mod base64;
pub mod cipherstring;
pub mod config;
pub mod db;
diff --git a/src/locked.rs b/src/locked.rs
index 4ddf021..bfa642d 100644
--- a/src/locked.rs
+++ b/src/locked.rs
@@ -18,10 +18,12 @@ impl Default for Vec {
}
impl Vec {
+ #[must_use]
pub fn new() -> Self {
Self::default()
}
+ #[must_use]
pub fn data(&self) -> &[u8] {
self.data.as_slice()
}
@@ -65,10 +67,12 @@ pub struct Password {
}
impl Password {
+ #[must_use]
pub fn new(password: Vec) -> Self {
Self { password }
}
+ #[must_use]
pub fn password(&self) -> &[u8] {
self.password.data()
}
@@ -80,14 +84,17 @@ pub struct Keys {
}
impl Keys {
+ #[must_use]
pub fn new(keys: Vec) -> Self {
Self { keys }
}
+ #[must_use]
pub fn enc_key(&self) -> &[u8] {
&self.keys.data()[0..32]
}
+ #[must_use]
pub fn mac_key(&self) -> &[u8] {
&self.keys.data()[32..64]
}
@@ -99,10 +106,12 @@ pub struct PasswordHash {
}
impl PasswordHash {
+ #[must_use]
pub fn new(hash: Vec) -> Self {
Self { hash }
}
+ #[must_use]
pub fn hash(&self) -> &[u8] {
self.hash.data()
}
@@ -114,10 +123,12 @@ pub struct PrivateKey {
}
impl PrivateKey {
+ #[must_use]
pub fn new(private_key: Vec) -> Self {
Self { private_key }
}
+ #[must_use]
pub fn private_key(&self) -> &[u8] {
self.private_key.data()
}
@@ -130,6 +141,7 @@ pub struct ApiKey {
}
impl ApiKey {
+ #[must_use]
pub fn new(client_id: Password, client_secret: Password) -> Self {
Self {
client_id,
@@ -137,10 +149,12 @@ impl ApiKey {
}
}
+ #[must_use]
pub fn client_id(&self) -> &[u8] {
self.client_id.password()
}
+ #[must_use]
pub fn client_secret(&self) -> &[u8] {
self.client_secret.password()
}
diff --git a/src/pinentry.rs b/src/pinentry.rs
index b4d2bb0..e2a83ed 100644
--- a/src/pinentry.rs
+++ b/src/pinentry.rs
@@ -1,5 +1,6 @@
use crate::prelude::*;
+use std::convert::TryFrom as _;
use tokio::io::AsyncWriteExt as _;
pub async fn getpin(
@@ -33,18 +34,18 @@ pub async fn getpin(
.map_err(|source| Error::WriteStdin { source })?;
ncommands += 1;
stdin
- .write_all(format!("SETPROMPT {}\n", prompt).as_bytes())
+ .write_all(format!("SETPROMPT {prompt}\n").as_bytes())
.await
.map_err(|source| Error::WriteStdin { source })?;
ncommands += 1;
stdin
- .write_all(format!("SETDESC {}\n", desc).as_bytes())
+ .write_all(format!("SETDESC {desc}\n").as_bytes())
.await
.map_err(|source| Error::WriteStdin { source })?;
ncommands += 1;
if let Some(err) = err {
stdin
- .write_all(format!("SETERROR {}\n", err).as_bytes())
+ .write_all(format!("SETERROR {err}\n").as_bytes())
.await
.map_err(|source| Error::WriteStdin { source })?;
ncommands += 1;
@@ -76,15 +77,13 @@ pub async fn getpin(
Ok(crate::locked::Password::new(buf))
}
-async fn read_password<
- R: tokio::io::AsyncRead + tokio::io::AsyncReadExt + Unpin,
->(
+async fn read_password<R>(
mut ncommands: u8,
data: &mut [u8],
mut r: R,
) -> Result<usize>
where
- R: Send,
+ R: tokio::io::AsyncRead + tokio::io::AsyncReadExt + Unpin + Send,
{
let mut len = 0;
loop {
@@ -119,7 +118,7 @@ where
});
}
return Err(Error::PinentryErrorMessage {
- error: format!("unknown error ({})", code),
+ error: format!("unknown error ({code})"),
});
}
None => {
@@ -138,6 +137,14 @@ where
.read(&mut data[len..])
.await
.map_err(|source| Error::PinentryReadOutput { source })?;
+ if bytes == 0 {
+ return Err(Error::PinentryReadOutput {
+ source: std::io::Error::new(
+ std::io::ErrorKind::UnexpectedEof,
+ "unexpected EOF",
+ ),
+ });
+ }
len += bytes;
}
}
@@ -161,9 +168,11 @@ fn percent_decode(buf: &mut [u8]) -> usize {
if c == b'%' && read_idx + 2 < len {
if let Some(h) = char::from(buf[read_idx + 1]).to_digit(16) {
- #[allow(clippy::cast_possible_truncation)]
if let Some(l) = char::from(buf[read_idx + 2]).to_digit(16) {
- c = h as u8 * 0x10 + l as u8;
+ // h and l were parsed from a single hex digit, so they
+ // must be in the range 0-15, so these unwraps are safe
+ c = u8::try_from(h).unwrap() * 0x10
+ + u8::try_from(l).unwrap();
read_idx += 2;
}
}
diff --git a/src/protocol.rs b/src/protocol.rs
index 14fa7f9..e883441 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -1,8 +1,6 @@
-// https://github.com/rust-lang/rust-clippy/issues/6902
-#![allow(clippy::use_self)]
-
// eventually it would be nice to make this a const function so that we could
// just get the version from a variable directly, but this is fine for now
+#[must_use]
pub fn version() -> u32 {
let major = env!("CARGO_PKG_VERSION_MAJOR");
let minor = env!("CARGO_PKG_VERSION_MINOR");
@@ -36,6 +34,9 @@ pub enum Action {
plaintext: String,
org_id: Option<String>,
},
+ ClipboardStore {
+ text: String,
+ },
Quit,
Version,
}
diff --git a/src/pwgen.rs b/src/pwgen.rs
index 55151e6..a112d73 100644
--- a/src/pwgen.rs
+++ b/src/pwgen.rs
@@ -15,6 +15,7 @@ pub enum Type {
Diceware,
}
+#[must_use]
pub fn pwgen(ty: Type, len: usize) -> String {
let mut rng = rand::thread_rng();
@@ -101,6 +102,6 @@ mod test {
for c in s.chars() {
set.insert(c);
}
- assert!(set.len() < s.len())
+ assert!(set.len() < s.len());
}
}