diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index e1aeea244..efa8c7bf0 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -102,7 +102,7 @@ jobs: run: ./build-schemas.sh - name: Publish - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0 with: arguments: sdk:publish build-root-directory: languages/kotlin diff --git a/.github/workflows/build-java.yml b/.github/workflows/build-java.yml index 0ed8997ca..2559f659c 100644 --- a/.github/workflows/build-java.yml +++ b/.github/workflows/build-java.yml @@ -61,7 +61,7 @@ jobs: path: languages/java/src/main/resources/win32-x86-64 - name: Publish Maven - uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0 with: arguments: publish build-root-directory: languages/java diff --git a/.github/workflows/publish-ruby.yml b/.github/workflows/publish-ruby.yml index 81aedcc22..12abd18f0 100644 --- a/.github/workflows/publish-ruby.yml +++ b/.github/workflows/publish-ruby.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Ruby - uses: ruby/setup-ruby@bd03e04863f52d169e18a2b190e8fa6b84938215 # v1.170.0 + uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0 with: ruby-version: 3.2 diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 93a2ddecf..8408dd1d7 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -73,7 +73,7 @@ jobs: run: cargo llvm-cov --all-features --lcov --output-path lcov.info --ignore-filename-regex "crates/bitwarden-api-" - name: Upload to codecov.io - uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # v3.1.5 + uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4.0.1 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 8ebf52136..98274e54f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" [[package]] name = "anstyle-parse" @@ -612,7 +612,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", - "toml 0.8.8", + "toml 0.8.9", "uuid", ] @@ -752,9 +752,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.9" +version = "4.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df631ae429f6613fcd3a7c1adbdb65f637271e561b03680adaa6573015dfb106" +checksum = "abb745187d7f4d76267b37485a65e0149edd0e91a4cfcdd3f27524ad86cee9f3" dependencies = [ "clap", ] @@ -1242,9 +1242,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -1502,7 +1502,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.1", + "indexmap 2.2.2", "slab", "tokio", "tokio-util", @@ -1661,9 +1661,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.59" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1717,9 +1717,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1778,22 +1778,11 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "is-terminal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.52.0", -] - [[package]] name = "is_ci" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "itertools" @@ -1806,9 +1795,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -1859,9 +1848,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" @@ -1960,9 +1949,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -1981,9 +1970,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.15.0" +version = "2.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbf98e1bcb85cc441bbf7cdfb11070d2537a100e2697d75397b2584c32492d1" +checksum = "43792514b0c95c5beec42996da0c1b39265b02b75c97baa82d163d3ef55cbfa7" dependencies = [ "bitflags 2.4.2", "ctor", @@ -2093,6 +2082,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.45" @@ -2326,7 +2321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" dependencies = [ "base64 0.21.7", - "indexmap 2.2.1", + "indexmap 2.2.2", "line-wrap", "quick-xml", "serde", @@ -2603,9 +2598,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "base64 0.21.7", "bytes", @@ -2630,6 +2625,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-rustls", @@ -2699,9 +2695,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -2893,7 +2889,7 @@ dependencies = [ "bitwarden", "bitwarden-json", "bitwarden-uniffi", - "itertools 0.12.0", + "itertools 0.12.1", "schemars", "serde_json", ] @@ -3033,7 +3029,7 @@ version = "0.9.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adf8a49373e98a4c5f0ceb5d05aa7c648d75f63774981ed95b7c7443bbd50c6e" dependencies = [ - "indexmap 2.2.1", + "indexmap 2.2.2", "itoa", "ryu", "serde", @@ -3215,11 +3211,10 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "supports-color" -version = "2.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" dependencies = [ - "is-terminal", "is_ci", ] @@ -3245,6 +3240,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "syntect" version = "5.1.0" @@ -3347,12 +3348,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -3367,10 +3369,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -3391,9 +3394,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -3452,9 +3455,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325" dependencies = [ "serde", "serde_spanned", @@ -3473,11 +3476,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.1", + "indexmap 2.2.2", "serde", "serde_spanned", "toml_datetime", @@ -3932,9 +3935,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "weedle2" @@ -4119,9 +4122,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.35" +version = "0.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d" +checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5" dependencies = [ "memchr", ] diff --git a/crates/bitwarden-cli/Cargo.toml b/crates/bitwarden-cli/Cargo.toml index bded30904..4248c9189 100644 --- a/crates/bitwarden-cli/Cargo.toml +++ b/crates/bitwarden-cli/Cargo.toml @@ -8,4 +8,4 @@ rust-version = "1.57" clap = { version = "4.4.18", features = ["derive"] } color-eyre = "0.6" inquire = "0.6.2" -supports-color = "2.1.0" +supports-color = "3.0.0" diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index 04768db9e..f9bda838a 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -206,6 +206,8 @@ impl schemars::JsonSchema for AsymmetricEncString { #[cfg(test)] mod tests { + use schemars::schema_for; + use super::{AsymmetricCryptoKey, AsymmetricEncString, KeyDecryptable}; const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY----- @@ -288,4 +290,35 @@ XKZBokBGnjFnTnKcs7nv/O8= assert_eq!(t.key.to_string(), cipher); assert_eq!(serde_json::to_string(&t).unwrap(), serialized); } + + #[test] + fn test_from_str_invalid() { + let enc_str = "7.ABC"; + let enc_string: Result = enc_str.parse(); + + let err = enc_string.unwrap_err(); + assert_eq!( + err.to_string(), + "EncString error, Invalid asymmetric type, got type 7 with 1 parts" + ); + } + + #[test] + fn test_debug_format() { + let enc_str: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw=="; + let enc_string: AsymmetricEncString = enc_str.parse().unwrap(); + + let debug_string = format!("{:?}", enc_string); + assert_eq!(debug_string, "AsymmetricEncString"); + } + + #[test] + fn test_json_schema() { + let schema = schema_for!(AsymmetricEncString); + + assert_eq!( + serde_json::to_string(&schema).unwrap(), + r#"{"$schema":"http://json-schema.org/draft-07/schema#","title":"AsymmetricEncString","type":"string"}"# + ); + } } diff --git a/crates/bitwarden-crypto/src/enc_string/mod.rs b/crates/bitwarden-crypto/src/enc_string/mod.rs index e1433821f..3250c1a58 100644 --- a/crates/bitwarden-crypto/src/enc_string/mod.rs +++ b/crates/bitwarden-crypto/src/enc_string/mod.rs @@ -75,3 +75,56 @@ where T::from_str(v).map_err(|e| E::custom(format!("{:?}", e))) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_check_length_less_than_expected() { + let buf = [1, 2, 3]; + let expected = 5; + let result = check_length(&buf, expected); + assert!(result.is_err()); + } + + #[test] + fn test_check_length_equal_to_expected() { + let buf = [1, 2, 3, 4, 5]; + let expected = 5; + let result = check_length(&buf, expected); + assert!(result.is_ok()); + } + + #[test] + fn test_check_length_greater_than_expected() { + let buf = [1, 2, 3, 4, 5, 6]; + let expected = 5; + let result = check_length(&buf, expected); + assert!(result.is_ok()); + } + + #[test] + fn test_split_enc_string_new_format() { + let s = "2.abc|def|ghi"; + let (header, parts) = split_enc_string(s); + assert_eq!(header, "2"); + assert_eq!(parts, vec!["abc", "def", "ghi"]); + } + + #[test] + fn test_split_enc_string_old_format_three_parts() { + let s = "abc|def|ghi"; + let (header, parts) = split_enc_string(s); + assert_eq!(header, "1"); + assert_eq!(parts, vec!["abc", "def", "ghi"]); + } + + #[test] + fn test_split_enc_string_old_format_fewer_parts() { + let s = "abc|def"; + let (header, parts) = split_enc_string(s); + assert_eq!(header, "0"); + assert_eq!(parts, vec!["abc", "def"]); + } +} diff --git a/crates/bitwarden-crypto/src/enc_string/symmetric.rs b/crates/bitwarden-crypto/src/enc_string/symmetric.rs index a76315e96..a7768de23 100644 --- a/crates/bitwarden-crypto/src/enc_string/symmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/symmetric.rs @@ -274,6 +274,8 @@ impl schemars::JsonSchema for EncString { #[cfg(test)] mod tests { + use schemars::schema_for; + use super::EncString; use crate::{derive_symmetric_key, KeyDecryptable, KeyEncryptable}; @@ -325,4 +327,78 @@ mod tests { assert_eq!(enc_string_new.to_string(), enc_str) } + + #[test] + fn test_from_str_cbc256() { + let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w=="; + let enc_string: EncString = enc_str.parse().unwrap(); + + assert_eq!(enc_string.enc_type(), 0); + if let EncString::AesCbc256_B64 { iv, data } = &enc_string { + assert_eq!( + iv, + &[164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150] + ); + assert_eq!( + data, + &[93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215] + ); + } + } + + #[test] + fn test_from_str_cbc128_hmac() { + let enc_str = "1.Hh8gISIjJCUmJygpKissLQ==|MjM0NTY3ODk6Ozw9Pj9AQUJDREU=|KCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkc="; + let enc_string: EncString = enc_str.parse().unwrap(); + + assert_eq!(enc_string.enc_type(), 1); + if let EncString::AesCbc128_HmacSha256_B64 { iv, mac, data } = &enc_string { + assert_eq!( + iv, + &[30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45] + ); + assert_eq!( + mac, + &[ + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71 + ] + ); + assert_eq!( + data, + &[50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69] + ); + } + } + + #[test] + fn test_from_str_invalid() { + let enc_str = "7.ABC"; + let enc_string: Result = enc_str.parse(); + + let err = enc_string.unwrap_err(); + assert_eq!( + err.to_string(), + "EncString error, Invalid symmetric type, got type 7 with 1 parts" + ); + } + + #[test] + fn test_debug_format() { + let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8="; + let enc_string: EncString = enc_str.parse().unwrap(); + + let debug_string = format!("{:?}", enc_string); + assert_eq!(debug_string, "EncString"); + } + + #[test] + fn test_json_schema() { + let schema = schema_for!(EncString); + + assert_eq!( + serde_json::to_string(&schema).unwrap(), + r#"{"$schema":"http://json-schema.org/draft-07/schema#","title":"EncString","type":"string"}"# + ); + } } diff --git a/crates/bitwarden-crypto/src/keys/master_key.rs b/crates/bitwarden-crypto/src/keys/master_key.rs index 920f103e4..0a435ed88 100644 --- a/crates/bitwarden-crypto/src/keys/master_key.rs +++ b/crates/bitwarden-crypto/src/keys/master_key.rs @@ -39,6 +39,10 @@ pub enum HashPurpose { pub struct MasterKey(SymmetricCryptoKey); impl MasterKey { + pub fn new(key: SymmetricCryptoKey) -> MasterKey { + Self(key) + } + /// Derives a users master key from their password, email and KDF. pub fn derive(password: &[u8], email: &[u8], kdf: &Kdf) -> Result { derive_key(password, email, kdf).map(Self) diff --git a/crates/bitwarden-generators/Cargo.toml b/crates/bitwarden-generators/Cargo.toml index 7f26a47cf..39eeb3d71 100644 --- a/crates/bitwarden-generators/Cargo.toml +++ b/crates/bitwarden-generators/Cargo.toml @@ -29,5 +29,5 @@ uniffi = { version = "=0.26.1", optional = true } [dev-dependencies] rand_chacha = "0.3.1" -tokio = { version = "1.35.1", features = ["rt", "macros"] } +tokio = { version = "1.36.0", features = ["rt", "macros"] } wiremock = "0.5.22" diff --git a/crates/bitwarden-napi/package-lock.json b/crates/bitwarden-napi/package-lock.json index a8995bc77..9c9d87de9 100644 --- a/crates/bitwarden-napi/package-lock.json +++ b/crates/bitwarden-napi/package-lock.json @@ -95,9 +95,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz", - "integrity": "sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==", + "version": "20.11.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", "dev": true, "peer": true, "dependencies": { diff --git a/crates/bitwarden-napi/tsconfig.json b/crates/bitwarden-napi/tsconfig.json index 8ec7e00d4..f977e0759 100644 --- a/crates/bitwarden-napi/tsconfig.json +++ b/crates/bitwarden-napi/tsconfig.json @@ -7,7 +7,7 @@ "strict": true, "noImplicitAny": true, "esModuleInterop": true, - "declaration": true, + "declaration": true }, - "include": ["src-ts", "src-ts/bitwarden_client", "src-ts/index.ts"], + "include": ["src-ts", "src-ts/bitwarden_client", "src-ts/index.ts"] } diff --git a/crates/bitwarden-py/Cargo.toml b/crates/bitwarden-py/Cargo.toml index ed7c5df5e..e81f2f5a8 100644 --- a/crates/bitwarden-py/Cargo.toml +++ b/crates/bitwarden-py/Cargo.toml @@ -18,7 +18,7 @@ bitwarden-json = { path = "../bitwarden-json", features = ["secrets"] } pyo3-build-config = { version = "0.20.2" } [target.'cfg(not(target_arch="wasm32"))'.dependencies] -tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } pyo3-asyncio = { version = "0.20.0", features = [ "attributes", "tokio-runtime", diff --git a/crates/bitwarden-uniffi/src/auth/mod.rs b/crates/bitwarden-uniffi/src/auth/mod.rs index 62c791967..75e0c5656 100644 --- a/crates/bitwarden-uniffi/src/auth/mod.rs +++ b/crates/bitwarden-uniffi/src/auth/mod.rs @@ -90,8 +90,27 @@ impl ClientAuth { .write() .await .auth() - .validate_password(password, password_hash.to_string()) - .await?) + .validate_password(password, password_hash.to_string())?) + } + + /// Validate the user password without knowing the password hash + /// + /// Used for accounts that we know have master passwords but that have not logged in with a + /// password. Some example are login with device or TDE. + /// + /// This works by comparing the provided password against the encrypted user key. + pub async fn validate_password_user_key( + &self, + password: String, + encrypted_user_key: String, + ) -> Result { + Ok(self + .0 + .0 + .write() + .await + .auth() + .validate_password_user_key(password, encrypted_user_key)?) } /// Initialize a new auth request diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index b52e2f8f3..d9508258b 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -76,6 +76,6 @@ reqwest = { version = "*", features = [ [dev-dependencies] rand_chacha = "0.3.1" -tokio = { version = "1.35.1", features = ["rt", "macros"] } +tokio = { version = "1.36.0", features = ["rt", "macros"] } wiremock = "0.5.22" zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] } diff --git a/crates/bitwarden/src/auth/api/request/auth_request_token_request.rs b/crates/bitwarden/src/auth/api/request/auth_request_token_request.rs new file mode 100644 index 000000000..cf5ae7ee4 --- /dev/null +++ b/crates/bitwarden/src/auth/api/request/auth_request_token_request.rs @@ -0,0 +1,59 @@ +use log::debug; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + auth::api::response::IdentityTokenResponse, + client::{client_settings::DeviceType, ApiConfigurations}, + error::Result, +}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthRequestTokenRequest { + scope: String, + client_id: String, + #[serde(rename = "deviceType")] + device_type: u8, + #[serde(rename = "deviceIdentifier")] + device_identifier: String, + #[serde(rename = "deviceName")] + device_name: String, + grant_type: String, + #[serde(rename = "username")] + email: String, + #[serde(rename = "authRequest")] + auth_request_id: Uuid, + #[serde(rename = "password")] + access_code: String, +} + +impl AuthRequestTokenRequest { + pub fn new( + email: &str, + auth_request_id: &Uuid, + access_code: &str, + device_type: DeviceType, + device_identifier: &str, + ) -> Self { + let obj = Self { + scope: "api offline_access".to_string(), + client_id: "web".to_string(), + device_type: device_type as u8, + device_identifier: device_identifier.to_string(), + device_name: "chrome".to_string(), + grant_type: "password".to_string(), + email: email.to_string(), + auth_request_id: *auth_request_id, + access_code: access_code.to_string(), + }; + debug!("initializing {:?}", obj); + obj + } + + pub(crate) async fn send( + &self, + configurations: &ApiConfigurations, + ) -> Result { + super::send_identity_connect_request(configurations, Some(&self.email), &self).await + } +} diff --git a/crates/bitwarden/src/auth/api/request/mod.rs b/crates/bitwarden/src/auth/api/request/mod.rs index 67796f2f3..2b5bde225 100644 --- a/crates/bitwarden/src/auth/api/request/mod.rs +++ b/crates/bitwarden/src/auth/api/request/mod.rs @@ -15,6 +15,11 @@ pub(crate) use password_token_request::*; #[cfg(feature = "internal")] pub(crate) use renew_token_request::*; +#[cfg(feature = "mobile")] +mod auth_request_token_request; +#[cfg(feature = "mobile")] +pub(crate) use auth_request_token_request::*; + use crate::{ auth::api::response::{parse_identity_response, IdentityTokenResponse}, client::ApiConfigurations, diff --git a/crates/bitwarden/src/auth/api/request/password_token_request.rs b/crates/bitwarden/src/auth/api/request/password_token_request.rs index fd016d898..2f6414bcd 100644 --- a/crates/bitwarden/src/auth/api/request/password_token_request.rs +++ b/crates/bitwarden/src/auth/api/request/password_token_request.rs @@ -6,7 +6,7 @@ use crate::{ api::response::IdentityTokenResponse, login::{TwoFactorProvider, TwoFactorRequest}, }, - client::ApiConfigurations, + client::{client_settings::DeviceType, ApiConfigurations}, error::Result, }; @@ -35,13 +35,19 @@ pub struct PasswordTokenRequest { } impl PasswordTokenRequest { - pub fn new(email: &str, password_hash: &String, two_factor: &Option) -> Self { + pub fn new( + email: &str, + password_hash: &str, + device_type: DeviceType, + device_identifier: &str, + two_factor: &Option, + ) -> Self { let tf = two_factor.as_ref(); let obj = Self { scope: "api offline_access".to_string(), client_id: "web".to_string(), - device_type: 10, - device_identifier: "b86dd6ab-4265-4ddf-a7f1-eb28d5677f33".to_string(), + device_type: device_type as u8, + device_identifier: device_identifier.to_string(), device_name: "firefox".to_string(), grant_type: "password".to_string(), master_password_hash: password_hash.to_string(), diff --git a/crates/bitwarden/src/auth/auth_request.rs b/crates/bitwarden/src/auth/auth_request.rs index 98fe4996a..18c71afba 100644 --- a/crates/bitwarden/src/auth/auth_request.rs +++ b/crates/bitwarden/src/auth/auth_request.rs @@ -3,7 +3,7 @@ use bitwarden_crypto::{ fingerprint, AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, }; #[cfg(feature = "mobile")] -use bitwarden_crypto::{KeyDecryptable, SymmetricCryptoKey}; +use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; use bitwarden_generators::{password, PasswordGeneratorRequest}; use crate::{error::Error, Client}; @@ -63,6 +63,22 @@ pub(crate) fn auth_request_decrypt_user_key( Ok(SymmetricCryptoKey::try_from(key.as_mut_slice())?) } +/// Decrypt the user key using the private key generated previously. +#[cfg(feature = "mobile")] +pub(crate) fn auth_request_decrypt_master_key( + private_key: String, + master_key: AsymmetricEncString, + user_key: EncString, +) -> Result { + use bitwarden_crypto::MasterKey; + + let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; + let mut master_key: Vec = master_key.decrypt_with_key(&key)?; + let master_key = MasterKey::new(SymmetricCryptoKey::try_from(master_key.as_mut_slice())?); + + Ok(master_key.decrypt_user_key(user_key)?) +} + /// Approve an auth request. /// /// Encrypts the user key with a public key. @@ -113,14 +129,14 @@ mod tests { use super::*; use crate::{ client::{LoginMethod, UserLoginMethod}, - mobile::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, + mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, }; #[test] fn test_approve() { let mut client = Client::new(None); client.set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: "123".to_owned(), + client_id: "7b821276-e27c-400b-9853-606393c87f18".to_owned(), email: "test@bitwarden.com".to_owned(), kdf: Kdf::PBKDF2 { iterations: NonZeroU32::new(600_000).unwrap(), @@ -133,16 +149,53 @@ mod tests { .initialize_user_crypto("asdfasdfasdf", user_key, private_key) .unwrap(); - let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnRtpYLp9QLaEUkdPkWZX6TrMUKFoSaFamBKDL0NlS6xwtETTqYIxRVsvnHii3Dhz+fh3aHQVyBa1rBXogeH3MLERzNADwZhpWtBT9wKCXY5o0fIWYdZV/Nf0Y+0ZoKdImrGPLPmyHGfCqrvrK7g09q8+3kXUlkdAImlQqc5TiYwiHBfUQVTBq/Ae7a0FEpajx1NUM4h3edpCYxbvnpSTuzMgbmbUUS4gdCaheA2ibYxy/zkLzsaLygoibMyGNl9Y8J5n7dDrVXpUKZTihVfXwHfEZwtKNunWsmmt8rEJWVpguUDEDVSUogoxQcNaCi7KHn9ioSip76hg1jLpypO3WwIDAQAB"; + let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB"; // Verify fingerprint let pbkey = STANDARD.decode(public_key).unwrap(); let fingerprint = fingerprint("test@bitwarden.com", &pbkey).unwrap(); - assert_eq!(fingerprint, "spill-applaud-sweep-habitable-shrunk"); + assert_eq!(fingerprint, "childless-unfair-prowler-dropbox-designate"); approve_auth_request(&mut client, public_key.to_owned()).unwrap(); } + #[tokio::test] + async fn test_decrypt_user_key() { + let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; + + let enc_user_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); + let dec = auth_request_decrypt_user_key(private_key.to_owned(), enc_user_key).unwrap(); + + assert_eq!( + dec.to_vec().as_ref(), + vec![ + 201, 37, 234, 213, 21, 75, 40, 70, 149, 213, 234, 16, 19, 251, 162, 245, 161, 74, + 34, 245, 211, 151, 211, 192, 95, 10, 117, 50, 88, 223, 23, 157 + ] + ); + } + + #[tokio::test] + async fn test_decrypt_master_key() { + let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; + + let enc_master_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); + let enc_user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); + let dec = + auth_request_decrypt_master_key(private_key.to_owned(), enc_master_key, enc_user_key) + .unwrap(); + + assert_eq!( + dec.to_vec().as_ref(), + vec![ + 109, 128, 172, 147, 206, 123, 134, 95, 16, 36, 155, 113, 201, 18, 186, 230, 216, + 212, 173, 188, 74, 11, 134, 131, 137, 242, 105, 178, 105, 126, 52, 139, 248, 91, + 215, 21, 128, 91, 226, 222, 165, 67, 251, 34, 83, 81, 77, 147, 225, 76, 13, 41, + 102, 45, 183, 218, 106, 89, 254, 208, 251, 101, 130, 10, + ] + ); + } + #[tokio::test] async fn test_device_login() { let kdf = Kdf::PBKDF2 { @@ -181,7 +234,9 @@ mod tests { private_key: private_key.to_owned(), method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, - protected_user_key: approved_req, + method: AuthRequestMethod::UserKey { + protected_user_key: approved_req, + }, }, }) .await diff --git a/crates/bitwarden/src/auth/client_auth.rs b/crates/bitwarden/src/auth/client_auth.rs index aaa387741..f08daf0b7 100644 --- a/crates/bitwarden/src/auth/client_auth.rs +++ b/crates/bitwarden/src/auth/client_auth.rs @@ -1,6 +1,8 @@ #[cfg(feature = "internal")] use bitwarden_crypto::{AsymmetricEncString, DeviceKey, TrustDeviceResponse}; +#[cfg(feature = "mobile")] +use crate::auth::login::NewAuthRequestResponse; #[cfg(feature = "secrets")] use crate::auth::login::{login_access_token, AccessTokenLoginRequest, AccessTokenLoginResponse}; use crate::{auth::renew::renew_token, error::Result, Client}; @@ -14,7 +16,8 @@ use crate::{ TwoFactorEmailRequest, }, password::{ - password_strength, satisfies_policy, validate_password, MasterPasswordPolicyOptions, + password_strength, satisfies_policy, validate_password, validate_password_user_key, + MasterPasswordPolicyOptions, }, register::{make_register_keys, register}, AuthRequestResponse, RegisterKeyResponse, RegisterRequest, @@ -99,8 +102,16 @@ impl<'a> ClientAuth<'a> { send_two_factor_email(self.client, tf).await } - pub async fn validate_password(&self, password: String, password_hash: String) -> Result { - validate_password(self.client, password, password_hash).await + pub fn validate_password(&self, password: String, password_hash: String) -> Result { + validate_password(self.client, password, password_hash) + } + + pub fn validate_password_user_key( + &self, + password: String, + encrypted_user_key: String, + ) -> Result { + validate_password_user_key(self.client, password, encrypted_user_key) } pub fn new_auth_request(&self, email: &str) -> Result { @@ -116,6 +127,25 @@ impl<'a> ClientAuth<'a> { } } +#[cfg(feature = "mobile")] +impl<'a> ClientAuth<'a> { + pub async fn login_device( + &mut self, + email: String, + device_identifier: String, + ) -> Result { + use crate::auth::login::send_new_auth_request; + + send_new_auth_request(self.client, email, device_identifier).await + } + + pub async fn login_device_complete(&mut self, auth_req: NewAuthRequestResponse) -> Result<()> { + use crate::auth::login::complete_auth_request; + + complete_auth_request(self.client, auth_req).await + } +} + #[cfg(feature = "internal")] fn trust_device(client: &Client) -> Result { let enc = client.get_encryption_settings()?; diff --git a/crates/bitwarden/src/auth/login/auth_request.rs b/crates/bitwarden/src/auth/login/auth_request.rs new file mode 100644 index 000000000..30db06124 --- /dev/null +++ b/crates/bitwarden/src/auth/login/auth_request.rs @@ -0,0 +1,131 @@ +use std::num::NonZeroU32; + +use bitwarden_api_api::{ + apis::auth_requests_api::{auth_requests_id_response_get, auth_requests_post}, + models::{AuthRequestCreateRequestModel, AuthRequestType}, +}; +use bitwarden_crypto::Kdf; +use uuid::Uuid; + +use crate::{ + auth::{ + api::{request::AuthRequestTokenRequest, response::IdentityTokenResponse}, + auth_request::new_auth_request, + }, + client::{LoginMethod, UserLoginMethod}, + error::Result, + mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, + Client, +}; + +pub struct NewAuthRequestResponse { + pub fingerprint: String, + email: String, + device_identifier: String, + auth_request_id: Uuid, + access_code: String, + private_key: String, +} + +pub(crate) async fn send_new_auth_request( + client: &mut Client, + email: String, + device_identifier: String, +) -> Result { + let config = client.get_api_configurations().await; + + let auth = new_auth_request(&email)?; + + let req = AuthRequestCreateRequestModel { + email: email.clone(), + public_key: auth.public_key, + device_identifier: device_identifier.clone(), + access_code: auth.access_code.clone(), + r#type: AuthRequestType::Variant0, // AuthenticateAndUnlock + }; + + let res = auth_requests_post(&config.api, Some(req)).await?; + + Ok(NewAuthRequestResponse { + fingerprint: auth.fingerprint, + email, + device_identifier, + auth_request_id: res.id.unwrap(), + access_code: auth.access_code, + private_key: auth.private_key, + }) +} + +pub(crate) async fn complete_auth_request( + client: &mut Client, + auth_req: NewAuthRequestResponse, +) -> Result<()> { + let config = client.get_api_configurations().await; + + let res = auth_requests_id_response_get( + &config.api, + auth_req.auth_request_id, + Some(&auth_req.access_code), + ) + .await?; + + let approved = res.request_approved.unwrap_or(false); + + if !approved { + return Err("Auth request was not approved".into()); + } + + let response = AuthRequestTokenRequest::new( + &auth_req.email, + &auth_req.auth_request_id, + &auth_req.access_code, + config.device_type, + &auth_req.device_identifier, + ) + .send(config) + .await?; + + if let IdentityTokenResponse::Authenticated(r) = response { + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }; + + client.set_tokens( + r.access_token.clone(), + r.refresh_token.clone(), + r.expires_in, + ); + client.set_login_method(LoginMethod::User(UserLoginMethod::Username { + client_id: "web".to_owned(), + email: auth_req.email.to_owned(), + kdf: kdf.clone(), + })); + + let method = match res.master_password_hash { + Some(_) => AuthRequestMethod::MasterKey { + protected_master_key: res.key.unwrap().parse().unwrap(), + auth_request_key: r.key.unwrap().parse().unwrap(), + }, + None => AuthRequestMethod::UserKey { + protected_user_key: res.key.unwrap().parse().unwrap(), + }, + }; + + client + .crypto() + .initialize_user_crypto(InitUserCryptoRequest { + kdf_params: kdf, + email: auth_req.email, + private_key: r.private_key.unwrap(), + method: InitUserCryptoMethod::AuthRequest { + request_private_key: auth_req.private_key, + method, + }, + }) + .await?; + + Ok(()) + } else { + Err("Failed to authenticate".into()) + } +} diff --git a/crates/bitwarden/src/auth/login/mod.rs b/crates/bitwarden/src/auth/login/mod.rs index afd7873a4..a36d27ae9 100644 --- a/crates/bitwarden/src/auth/login/mod.rs +++ b/crates/bitwarden/src/auth/login/mod.rs @@ -29,6 +29,13 @@ pub(crate) use api_key::login_api_key; #[cfg(feature = "internal")] pub use api_key::{ApiKeyLoginRequest, ApiKeyLoginResponse}; +#[cfg(feature = "mobile")] +mod auth_request; +#[cfg(feature = "mobile")] +pub use auth_request::NewAuthRequestResponse; +#[cfg(feature = "mobile")] +pub(crate) use auth_request::{complete_auth_request, send_new_auth_request}; + #[cfg(feature = "secrets")] mod access_token; #[cfg(feature = "secrets")] diff --git a/crates/bitwarden/src/auth/login/password.rs b/crates/bitwarden/src/auth/login/password.rs index f873ace97..02552b70e 100644 --- a/crates/bitwarden/src/auth/login/password.rs +++ b/crates/bitwarden/src/auth/login/password.rs @@ -34,8 +34,7 @@ pub(crate) async fn login_password( &input.kdf, &input.password, HashPurpose::ServerAuthorization, - ) - .await?; + )?; let response = request_identity_tokens(client, input, &password_hash).await?; if let IdentityTokenResponse::Authenticated(r) = &response { @@ -63,12 +62,20 @@ pub(crate) async fn login_password( async fn request_identity_tokens( client: &mut Client, input: &PasswordLoginRequest, - password_hash: &String, + password_hash: &str, ) -> Result { + use crate::client::client_settings::DeviceType; + let config = client.get_api_configurations().await; - PasswordTokenRequest::new(&input.email, password_hash, &input.two_factor) - .send(config) - .await + PasswordTokenRequest::new( + &input.email, + password_hash, + DeviceType::ChromeBrowser, + "b86dd6ab-4265-4ddf-a7f1-eb28d5677f33", + &input.two_factor, + ) + .send(config) + .await } #[cfg(feature = "internal")] diff --git a/crates/bitwarden/src/auth/login/two_factor.rs b/crates/bitwarden/src/auth/login/two_factor.rs index 45be042c7..c8f0cc55b 100644 --- a/crates/bitwarden/src/auth/login/two_factor.rs +++ b/crates/bitwarden/src/auth/login/two_factor.rs @@ -27,8 +27,7 @@ pub(crate) async fn send_two_factor_email( &kdf, &input.password, HashPurpose::ServerAuthorization, - ) - .await?; + )?; let config = client.get_api_configurations().await; bitwarden_api_api::apis::two_factor_api::two_factor_send_email_login_post( diff --git a/crates/bitwarden/src/auth/mod.rs b/crates/bitwarden/src/auth/mod.rs index 23b64eaf9..021c97c0f 100644 --- a/crates/bitwarden/src/auth/mod.rs +++ b/crates/bitwarden/src/auth/mod.rs @@ -14,16 +14,16 @@ use bitwarden_crypto::{HashPurpose, MasterKey}; pub use register::{RegisterKeyResponse, RegisterRequest}; #[cfg(feature = "internal")] mod auth_request; -#[cfg(feature = "mobile")] -pub(crate) use auth_request::auth_request_decrypt_user_key; #[cfg(feature = "internal")] pub use auth_request::AuthRequestResponse; +#[cfg(feature = "mobile")] +pub(crate) use auth_request::{auth_request_decrypt_master_key, auth_request_decrypt_user_key}; #[cfg(feature = "internal")] use crate::{client::Kdf, error::Result}; #[cfg(feature = "internal")] -async fn determine_password_hash( +fn determine_password_hash( email: &str, kdf: &Kdf, password: &str, @@ -40,8 +40,8 @@ mod tests { use super::*; #[cfg(feature = "internal")] - #[tokio::test] - async fn test_determine_password_hash() { + #[test] + fn test_determine_password_hash() { use super::determine_password_hash; let password = "password123"; @@ -51,9 +51,7 @@ mod tests { }; let purpose = HashPurpose::LocalAuthorization; - let result = determine_password_hash(email, &kdf, password, purpose) - .await - .unwrap(); + let result = determine_password_hash(email, &kdf, password, purpose).unwrap(); assert_eq!(result, "7kTqkF1pY/3JeOu73N9kR99fDDe9O1JOZaVc7KH3lsU="); } diff --git a/crates/bitwarden/src/auth/password/mod.rs b/crates/bitwarden/src/auth/password/mod.rs index b7833c7f8..d0f3329f2 100644 --- a/crates/bitwarden/src/auth/password/mod.rs +++ b/crates/bitwarden/src/auth/password/mod.rs @@ -3,5 +3,7 @@ pub(crate) use policy::satisfies_policy; pub use policy::MasterPasswordPolicyOptions; mod validate; pub(crate) use validate::validate_password; +#[cfg(feature = "internal")] +pub(crate) use validate::validate_password_user_key; mod strength; pub(crate) use strength::password_strength; diff --git a/crates/bitwarden/src/auth/password/validate.rs b/crates/bitwarden/src/auth/password/validate.rs index f6d22e11a..9003347d9 100644 --- a/crates/bitwarden/src/auth/password/validate.rs +++ b/crates/bitwarden/src/auth/password/validate.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::HashPurpose; +use bitwarden_crypto::{HashPurpose, MasterKey}; use crate::{ auth::determine_password_hash, @@ -8,7 +8,7 @@ use crate::{ }; /// Validate if the provided password matches the password hash stored in the client. -pub(crate) async fn validate_password( +pub(crate) fn validate_password( client: &Client, password: String, password_hash: String, @@ -22,9 +22,12 @@ pub(crate) async fn validate_password( match login_method { UserLoginMethod::Username { email, kdf, .. } | UserLoginMethod::ApiKey { email, kdf, .. } => { - let hash = - determine_password_hash(email, kdf, &password, HashPurpose::LocalAuthorization) - .await?; + let hash = determine_password_hash( + email, + kdf, + &password, + HashPurpose::LocalAuthorization, + )?; Ok(hash == password_hash) } @@ -34,13 +37,53 @@ pub(crate) async fn validate_password( } } +#[cfg(feature = "internal")] +pub(crate) fn validate_password_user_key( + client: &Client, + password: String, + encrypted_user_key: String, +) -> Result { + let login_method = client + .login_method + .as_ref() + .ok_or(Error::NotAuthenticated)?; + + if let LoginMethod::User(login_method) = login_method { + match login_method { + UserLoginMethod::Username { email, kdf, .. } + | UserLoginMethod::ApiKey { email, kdf, .. } => { + let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), kdf)?; + let user_key = master_key + .decrypt_user_key(encrypted_user_key.parse()?) + .map_err(|_| "wrong password")?; + + let enc = client + .get_encryption_settings() + .map_err(|_| Error::VaultLocked)?; + + let existing_key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + if user_key.to_vec() != existing_key.to_vec() { + return Err("wrong user key".into()); + } + + Ok(master_key + .derive_master_key_hash(password.as_bytes(), HashPurpose::LocalAuthorization)?) + } + } + } else { + Err(Error::NotAuthenticated) + } +} + #[cfg(test)] mod tests { - #[tokio::test] - async fn test_validate_password() { + use crate::auth::password::{validate::validate_password_user_key, validate_password}; + + #[test] + fn test_validate_password() { use std::num::NonZeroU32; - use super::validate_password; use crate::client::{Client, Kdf, LoginMethod, UserLoginMethod}; let mut client = Client::new(None); @@ -55,8 +98,68 @@ mod tests { let password = "password123".to_string(); let password_hash = "7kTqkF1pY/3JeOu73N9kR99fDDe9O1JOZaVc7KH3lsU=".to_string(); - let result = validate_password(&client, password, password_hash).await; + let result = validate_password(&client, password, password_hash); assert!(result.unwrap()); } + + #[cfg(feature = "internal")] + #[test] + fn test_validate_password_user_key() { + use std::num::NonZeroU32; + + use crate::client::{Client, Kdf, LoginMethod, UserLoginMethod}; + + let mut client = Client::new(None); + client.set_login_method(LoginMethod::User(UserLoginMethod::Username { + email: "test@bitwarden.com".to_string(), + kdf: Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }, + client_id: "1".to_string(), + })); + + let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + let private_key = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); + + client + .initialize_user_crypto("asdfasdfasdf", user_key.parse().unwrap(), private_key) + .unwrap(); + + let result = + validate_password_user_key(&client, "asdfasdfasdf".to_string(), user_key.to_string()) + .unwrap(); + + assert_eq!(result, "aOvkBXFhSdgrBWR3hZCMRoML9+h5yRblU3lFphCdkeA="); + assert!(validate_password(&client, "asdfasdfasdf".to_string(), result.to_string()).unwrap()) + } + + #[cfg(feature = "internal")] + #[test] + fn test_validate_password_user_key_wrong_password() { + use std::num::NonZeroU32; + + use crate::client::{Client, Kdf, LoginMethod, UserLoginMethod}; + + let mut client = Client::new(None); + client.set_login_method(LoginMethod::User(UserLoginMethod::Username { + email: "test@bitwarden.com".to_string(), + kdf: Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }, + client_id: "1".to_string(), + })); + + let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + let private_key = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); + + client + .initialize_user_crypto("asdfasdfasdf", user_key.parse().unwrap(), private_key) + .unwrap(); + + let result = validate_password_user_key(&client, "abc".to_string(), user_key.to_string()) + .unwrap_err(); + + assert_eq!(result.to_string(), "Internal error: wrong password"); + } } diff --git a/crates/bitwarden/src/client/client.rs b/crates/bitwarden/src/client/client.rs index 3ccb7f9ca..5b2c8b7e0 100644 --- a/crates/bitwarden/src/client/client.rs +++ b/crates/bitwarden/src/client/client.rs @@ -6,7 +6,7 @@ use bitwarden_crypto::SymmetricCryptoKey; #[cfg(feature = "internal")] use bitwarden_crypto::{AsymmetricEncString, EncString}; use chrono::Utc; -use reqwest::header::{self}; +use reqwest::header::{self, HeaderValue}; use uuid::Uuid; use super::AccessToken; @@ -29,6 +29,9 @@ use crate::{ pub(crate) struct ApiConfigurations { pub identity: bitwarden_api_identity::apis::configuration::Configuration, pub api: bitwarden_api_api::apis::configuration::Configuration, + /// Reqwest client useable for external integrations like email forwarders, HIBP. + #[allow(unused)] + pub external_client: reqwest::Client, pub device_type: DeviceType, } @@ -86,17 +89,28 @@ impl Client { pub fn new(settings_input: Option) -> Self { let settings = settings_input.unwrap_or_default(); - let headers = header::HeaderMap::new(); + fn new_client_builder() -> reqwest::ClientBuilder { + #[allow(unused_mut)] + let mut client_builder = reqwest::Client::builder(); - #[allow(unused_mut)] - let mut client_builder = reqwest::Client::builder().default_headers(headers); + #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))] + { + client_builder = + client_builder.use_preconfigured_tls(rustls_platform_verifier::tls_config()); + } - #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))] - { - client_builder = - client_builder.use_preconfigured_tls(rustls_platform_verifier::tls_config()); + client_builder } + let external_client = new_client_builder().build().unwrap(); + + let mut headers = header::HeaderMap::new(); + headers.append( + "Device-Type", + HeaderValue::from_str(&(settings.device_type as u8).to_string()).unwrap(), + ); + let client_builder = new_client_builder().default_headers(headers); + let client = client_builder.build().unwrap(); let identity = bitwarden_api_identity::apis::configuration::Configuration { @@ -127,6 +141,7 @@ impl Client { __api_configurations: ApiConfigurations { identity, api, + external_client, device_type: settings.device_type, }, encryption_settings: None, @@ -142,7 +157,7 @@ impl Client { #[cfg(feature = "mobile")] pub(crate) fn get_http_client(&self) -> &reqwest::Client { - &self.__api_configurations.api.client + &self.__api_configurations.external_client } #[cfg(feature = "secrets")] @@ -291,3 +306,23 @@ impl Client { Ok(self.encryption_settings.as_ref().unwrap()) } } + +#[cfg(test)] +mod tests { + #[test] + fn test_reqwest_rustls_platform_verifier_are_compatible() { + // rustls-platform-verifier is generating a rustls::ClientConfig, + // which reqwest accepts as a &dyn Any and then downcasts it to a + // rustls::ClientConfig. + + // This means that if the rustls version of the two crates don't match, + // the downcast will fail and we will get a runtime error. + + // This tests is added to ensure that it doesn't happen. + + let _ = reqwest::ClientBuilder::new() + .use_preconfigured_tls(rustls_platform_verifier::tls_config()) + .build() + .unwrap(); + } +} diff --git a/crates/bitwarden/src/mobile/crypto.rs b/crates/bitwarden/src/mobile/crypto.rs index 9b431b4b4..37bb3b905 100644 --- a/crates/bitwarden/src/mobile/crypto.rs +++ b/crates/bitwarden/src/mobile/crypto.rs @@ -54,14 +54,31 @@ pub enum InitUserCryptoMethod { AuthRequest { /// Private Key generated by the `crate::auth::new_auth_request`. request_private_key: String, + + method: AuthRequestMethod, + }, +} + +#[cfg(feature = "internal")] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum AuthRequestMethod { + UserKey { /// User Key protected by the private key provided in `AuthRequestResponse`. protected_user_key: AsymmetricEncString, }, + MasterKey { + /// Master Key protected by the private key provided in `AuthRequestResponse`. + protected_master_key: AsymmetricEncString, + /// User Key protected by the MasterKey, provided by the auth response. + auth_request_key: EncString, + }, } #[cfg(feature = "internal")] pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequest) -> Result<()> { - use crate::auth::auth_request_decrypt_user_key; + use crate::auth::{auth_request_decrypt_master_key, auth_request_decrypt_user_key}; let login_method = crate::client::LoginMethod::User(crate::client::UserLoginMethod::Username { client_id: "".to_string(), @@ -89,9 +106,21 @@ pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequ } InitUserCryptoMethod::AuthRequest { request_private_key, - protected_user_key, + method, } => { - let user_key = auth_request_decrypt_user_key(request_private_key, protected_user_key)?; + let user_key = match method { + AuthRequestMethod::UserKey { protected_user_key } => { + auth_request_decrypt_user_key(request_private_key, protected_user_key)? + } + AuthRequestMethod::MasterKey { + protected_master_key, + auth_request_key, + } => auth_request_decrypt_master_key( + request_private_key, + protected_master_key, + auth_request_key, + )?, + }; client.initialize_user_crypto_decrypted_key(user_key, private_key)?; } } diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index cc07397cd..1943e5ad5 100644 --- a/crates/bw/Cargo.toml +++ b/crates/bw/Cargo.toml @@ -18,7 +18,7 @@ color-eyre = "0.6" env_logger = "0.11.1" inquire = "0.6.2" log = "0.4.20" -tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } bitwarden = { path = "../bitwarden", version = "0.4.0", features = [ "internal", diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index 53b9b609e..e0195f5aa 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -114,3 +114,26 @@ pub(crate) async fn login_api_key( Ok(()) } + +pub(crate) async fn login_device( + mut client: Client, + email: Option, + device_identifier: Option, +) -> Result<()> { + let email = text_prompt_when_none("Email", email)?; + let device_identifier = text_prompt_when_none("Device Identifier", device_identifier)?; + + let auth = client + .auth() + .login_device(email, device_identifier) + .await + .unwrap(); + + println!("Fingerprint: {}", auth.fingerprint); + + Text::new("Press enter once approved").prompt()?; + + client.auth().login_device_complete(auth).await.unwrap(); + + Ok(()) +} diff --git a/crates/bw/src/auth/mod.rs b/crates/bw/src/auth/mod.rs index a4c7e2ed5..1f165f5f3 100644 --- a/crates/bw/src/auth/mod.rs +++ b/crates/bw/src/auth/mod.rs @@ -1,2 +1,2 @@ mod login; -pub(crate) use login::{login_api_key, login_password}; +pub(crate) use login::{login_api_key, login_device, login_password}; diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 0e7cd975e..6674bda1e 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -78,6 +78,11 @@ enum LoginCommands { client_id: Option, client_secret: Option, }, + Device { + #[arg(short = 'e', long, help = "Email address")] + email: Option, + device_identifier: Option, + }, } #[derive(Subcommand, Clone)] @@ -163,6 +168,12 @@ async fn process_commands() -> Result<()> { client_id, client_secret, } => auth::login_api_key(client, client_id, client_secret).await?, + LoginCommands::Device { + email, + device_identifier, + } => { + auth::login_device(client, email, device_identifier).await?; + } } return Ok(()); } diff --git a/crates/bws/Cargo.toml b/crates/bws/Cargo.toml index 1c04a6668..8acc9f92c 100644 --- a/crates/bws/Cargo.toml +++ b/crates/bws/Cargo.toml @@ -21,7 +21,7 @@ chrono = { version = "0.4.33", features = [ "std", ], default-features = false } clap = { version = "4.4.18", features = ["derive", "env", "string"] } -clap_complete = "4.4.9" +clap_complete = "4.4.10" color-eyre = "0.6" comfy-table = "^7.1.0" directories = "5.0.1" @@ -34,10 +34,10 @@ regex = { version = "1.10.3", features = [ serde = "^1.0.196" serde_json = "^1.0.113" serde_yaml = "0.9" -supports-color = "2.1.0" +supports-color = "3.0.0" thiserror = "1.0.56" -tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros"] } -toml = "0.8.8" +tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } +toml = "0.8.9" uuid = { version = "^1.7.0", features = ["serde"] } bitwarden = { path = "../bitwarden", version = "0.4.0", features = ["secrets"] } diff --git a/crates/sdk-schemas/Cargo.toml b/crates/sdk-schemas/Cargo.toml index 3e7282e7e..81a9d76ca 100644 --- a/crates/sdk-schemas/Cargo.toml +++ b/crates/sdk-schemas/Cargo.toml @@ -13,7 +13,7 @@ internal = [ [dependencies] anyhow = "1.0.79" -itertools = "0.12.0" +itertools = "0.12.1" schemars = { version = "0.8.16", features = ["preserve_order"] } serde_json = "1.0.113" diff --git a/languages/js/sdk-client/package-lock.json b/languages/js/sdk-client/package-lock.json index d1192e6f5..eb8d010db 100644 --- a/languages/js/sdk-client/package-lock.json +++ b/languages/js/sdk-client/package-lock.json @@ -39,9 +39,9 @@ } }, "node_modules/@types/node": { - "version": "18.19.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.10.tgz", - "integrity": "sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA==", + "version": "18.19.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", + "integrity": "sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index f85222738..d69e134c4 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -1280,9 +1280,9 @@ implementations. Private Key generated by the `crate::auth::new_auth_request`. - protected_user_key + method + - User Key protected by the private key provided in `AuthRequestResponse`. diff --git a/package-lock.json b/package-lock.json index 747d7d809..bdb82563e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@openapitools/openapi-generator-cli": "2.9.0", "handlebars": "^4.7.8", - "prettier": "3.2.4", + "prettier": "3.2.5", "quicktype-core": "23.0.81", "rimraf": "5.0.5", "ts-node": "10.9.2", @@ -346,9 +346,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz", - "integrity": "sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==", + "version": "20.11.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", "dev": true, "peer": true, "dependencies": { @@ -1471,9 +1471,9 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index 14b5692a0..05195270e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@openapitools/openapi-generator-cli": "2.9.0", "handlebars": "^4.7.8", - "prettier": "3.2.4", + "prettier": "3.2.5", "quicktype-core": "23.0.81", "rimraf": "5.0.5", "ts-node": "10.9.2",