From 565ed579db814ac386faa48a337a1c82cb7558b4 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Fri, 6 Oct 2023 14:22:44 +1000 Subject: [PATCH 01/15] Add auth_requests modules and basic list command --- .../src/admin_console/auth_requests/list.rs | 74 +++++++++++++++++++ .../src/admin_console/auth_requests/mod.rs | 6 ++ .../src/admin_console/client_auth_requests.rs | 24 ++++++ crates/bitwarden/src/admin_console/mod.rs | 3 + crates/bitwarden/src/lib.rs | 1 + 5 files changed, 108 insertions(+) create mode 100644 crates/bitwarden/src/admin_console/auth_requests/list.rs create mode 100644 crates/bitwarden/src/admin_console/auth_requests/mod.rs create mode 100644 crates/bitwarden/src/admin_console/client_auth_requests.rs create mode 100644 crates/bitwarden/src/admin_console/mod.rs diff --git a/crates/bitwarden/src/admin_console/auth_requests/list.rs b/crates/bitwarden/src/admin_console/auth_requests/list.rs new file mode 100644 index 000000000..f455b7fae --- /dev/null +++ b/crates/bitwarden/src/admin_console/auth_requests/list.rs @@ -0,0 +1,74 @@ +use bitwarden_api_api::models::{PendingOrganizationAuthRequestResponseModelListResponseModel, PendingOrganizationAuthRequestResponseModel}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + client::Client, + error::{Result, Error} +}; + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PendingAuthRequestsRequest { + /// Organization to retrieve pending auth requests for + pub organization_id: Uuid, +} + +pub(crate) async fn list_pending_requests( + client: &mut Client, + input: &PendingAuthRequestsRequest, +) -> Result { + let config = client.get_api_configurations().await; + let res = bitwarden_api_api::apis::organization_auth_requests_api::organizations_org_id_auth_requests_get( + &config.api, + input.organization_id, + ) + .await?; + + PendingAuthRequestsResponse::process_response(res) +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PendingAuthRequestsResponse { + pub data: Vec, +} + +impl PendingAuthRequestsResponse { + pub(crate) fn process_response( + response: PendingOrganizationAuthRequestResponseModelListResponseModel + ) -> Result { + Ok(PendingAuthRequestsResponse { + data: response + .data + .unwrap_or_default() + .into_iter() + .map(|r| PendingAuthRequestResponse::process_response(r)) + .collect::>()?, + }) + } +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PendingAuthRequestResponse { + pub id: Uuid, + pub user_id: Uuid, + pub organization_user_id: Uuid, + pub email: String, + +} + +impl PendingAuthRequestResponse { + pub(crate) fn process_response( + response: PendingOrganizationAuthRequestResponseModel + ) -> Result { + Ok(PendingAuthRequestResponse { + id: response.id.ok_or(Error::MissingFields)?, + user_id: response.user_id.ok_or(Error::MissingFields)?, + organization_user_id: response.organization_user_id.ok_or(Error::MissingFields)?, + email: response.email.ok_or(Error::MissingFields)?, + }) + } +} diff --git a/crates/bitwarden/src/admin_console/auth_requests/mod.rs b/crates/bitwarden/src/admin_console/auth_requests/mod.rs new file mode 100644 index 000000000..562cdfb7d --- /dev/null +++ b/crates/bitwarden/src/admin_console/auth_requests/mod.rs @@ -0,0 +1,6 @@ +mod list; + +pub(crate) use list::list_pending_requests; +pub use list::{ + PendingAuthRequestsRequest, PendingAuthRequestsResponse, PendingAuthRequestResponse +}; diff --git a/crates/bitwarden/src/admin_console/client_auth_requests.rs b/crates/bitwarden/src/admin_console/client_auth_requests.rs new file mode 100644 index 000000000..43c29e185 --- /dev/null +++ b/crates/bitwarden/src/admin_console/client_auth_requests.rs @@ -0,0 +1,24 @@ +use crate::{ + error::Result, + Client, + admin_console::auth_requests::{PendingAuthRequestsRequest, PendingAuthRequestsResponse, list_pending_requests} +}; + +pub struct ClientAuthRequests<'a> { + pub(crate) client: &'a mut crate::Client, +} + +impl<'a> ClientAuthRequests<'a> { + pub async fn list( + &mut self, + input: &PendingAuthRequestsRequest, + ) -> Result { + list_pending_requests(self.client, input).await + } +} + +impl<'a> Client { + pub fn client_auth_requests(&'a mut self) -> ClientAuthRequests<'a> { + ClientAuthRequests { client: self } + } +} diff --git a/crates/bitwarden/src/admin_console/mod.rs b/crates/bitwarden/src/admin_console/mod.rs new file mode 100644 index 000000000..67fb832bd --- /dev/null +++ b/crates/bitwarden/src/admin_console/mod.rs @@ -0,0 +1,3 @@ +pub mod auth_requests; + +mod client_auth_requests; diff --git a/crates/bitwarden/src/lib.rs b/crates/bitwarden/src/lib.rs index 31af93419..76f6a484a 100644 --- a/crates/bitwarden/src/lib.rs +++ b/crates/bitwarden/src/lib.rs @@ -69,6 +69,7 @@ mod util; #[cfg(feature = "mobile")] pub mod vault; pub mod wordlist; +pub mod admin_console; pub use client::Client; From f6114ee49146efc4973b950b3db6f76b003fbb7a Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Fri, 6 Oct 2023 15:44:40 +1000 Subject: [PATCH 02/15] WIP approve command --- .../admin_console/auth_requests/approve.rs | 129 ++++++++++++++++++ .../src/admin_console/auth_requests/list.rs | 2 +- .../src/admin_console/auth_requests/mod.rs | 1 + 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 crates/bitwarden/src/admin_console/auth_requests/approve.rs diff --git a/crates/bitwarden/src/admin_console/auth_requests/approve.rs b/crates/bitwarden/src/admin_console/auth_requests/approve.rs new file mode 100644 index 000000000..4a6f281c4 --- /dev/null +++ b/crates/bitwarden/src/admin_console/auth_requests/approve.rs @@ -0,0 +1,129 @@ +use bitwarden_api_api::models::{PendingOrganizationAuthRequestResponseModelListResponseModel, PendingOrganizationAuthRequestResponseModel, OrganizationUserResetPasswordDetailsResponseModel, AdminAuthRequestUpdateRequestModel}; +use rsa::pkcs8::DecodePrivateKey; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + client::Client, + error::{Result, Error, CryptoError}, + crypto::{EncString, Decryptable} +}; + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AuthApproveRequest { + /// ID of the auth request to approve + pub request_id: Uuid, + pub organization_user_id: Uuid, + pub organization_id: Uuid, + pub device_public_key: Uuid +} + +pub(crate) async fn approve_auth_request( + client: &mut Client, + input: &AuthApproveRequest, +) -> Result<()> { + let config = client.get_api_configurations().await; + + // Get user reset password details + let reset_password_details = + bitwarden_api_api::apis::organization_users_api::organizations_org_id_users_id_reset_password_details_get( + &config.api, + &input.organization_id.to_string(), + &input.request_id.to_string(), + ) + .await?; + + let encrypted_user_key = get_encrypted_user_key(client, input, reset_password_details)?; + + bitwarden_api_api::apis::organization_auth_requests_api::organizations_org_id_auth_requests_request_id_post( + &config.api, + input.organization_id, + input.request_id, + Some(AdminAuthRequestUpdateRequestModel { + encrypted_user_key: Some(encrypted_user_key.to_string()), + request_approved: true + }) + ) + .await?; + + Ok(()) +} + +fn get_encrypted_user_key( + client: &Client, + input: &AuthApproveRequest, + reset_password_details: OrganizationUserResetPasswordDetailsResponseModel) -> Result { + + // Decrypt organization's encrypted private key with org key + let enc = client.get_encryption_settings()?; + + let org_private_key = { + let dec = reset_password_details.encrypted_private_key + .ok_or(Error::MissingFields)? + .parse::()? + .decrypt(enc, &Some(input.organization_id))? + .into_bytes(); + + rsa::RsaPrivateKey::from_pkcs8_der(&dec) + .map_err(|_| CryptoError::InvalidKey)? + }; + + + // Decrypt user key with decrypted org private key + // TODO + // let user_key = user_reset_password_details_res.reset_password_key + // .ok_or(Error::MissingFields)? + // .parse::()? + // .decrypt_with_key(org_private_key) + // .decrypt(enc, &Some(input.organization_id))? + // .into_bytes(); + + // Re-encrypt the User Key with the Device Public Key + // return re-encrypted user key +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PendingAuthRequestsResponse { + pub data: Vec, +} + +impl PendingAuthRequestsResponse { + pub(crate) fn process_response( + response: PendingOrganizationAuthRequestResponseModelListResponseModel + ) -> Result { + Ok(PendingAuthRequestsResponse { + data: response + .data + .unwrap_or_default() + .into_iter() + .map(|r| PendingAuthRequestResponse::process_response(r)) + .collect::>()?, + }) + } +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PendingAuthRequestResponse { + pub id: Uuid, + pub user_id: Uuid, + pub organization_user_id: Uuid, + pub email: String, + // TODO: map rest of fields +} + +impl PendingAuthRequestResponse { + pub(crate) fn process_response( + response: PendingOrganizationAuthRequestResponseModel + ) -> Result { + Ok(PendingAuthRequestResponse { + id: response.id.ok_or(Error::MissingFields)?, + user_id: response.user_id.ok_or(Error::MissingFields)?, + organization_user_id: response.organization_user_id.ok_or(Error::MissingFields)?, + email: response.email.ok_or(Error::MissingFields)?, + }) + } +} diff --git a/crates/bitwarden/src/admin_console/auth_requests/list.rs b/crates/bitwarden/src/admin_console/auth_requests/list.rs index f455b7fae..587473f4f 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/list.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/list.rs @@ -57,7 +57,7 @@ pub struct PendingAuthRequestResponse { pub user_id: Uuid, pub organization_user_id: Uuid, pub email: String, - + // TODO: map rest of fields } impl PendingAuthRequestResponse { diff --git a/crates/bitwarden/src/admin_console/auth_requests/mod.rs b/crates/bitwarden/src/admin_console/auth_requests/mod.rs index 562cdfb7d..d6e535bf7 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/mod.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/mod.rs @@ -1,4 +1,5 @@ mod list; +mod approve; pub(crate) use list::list_pending_requests; pub use list::{ From 1516de1dbba5d6734aefe8e1552eaafc297bc510 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Mon, 9 Oct 2023 09:02:52 +1000 Subject: [PATCH 03/15] compiles but no idea if it works --- .../admin_console/auth_requests/approve.rs | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/crates/bitwarden/src/admin_console/auth_requests/approve.rs b/crates/bitwarden/src/admin_console/auth_requests/approve.rs index 4a6f281c4..e4d776975 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/approve.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/approve.rs @@ -1,5 +1,8 @@ use bitwarden_api_api::models::{PendingOrganizationAuthRequestResponseModelListResponseModel, PendingOrganizationAuthRequestResponseModel, OrganizationUserResetPasswordDetailsResponseModel, AdminAuthRequestUpdateRequestModel}; -use rsa::pkcs8::DecodePrivateKey; +use rsa::{ + pkcs8::DecodePrivateKey, + Pkcs1v15Encrypt, pkcs1::DecodeRsaPublicKey +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -17,28 +20,27 @@ pub struct AuthApproveRequest { pub request_id: Uuid, pub organization_user_id: Uuid, pub organization_id: Uuid, - pub device_public_key: Uuid + pub device_public_key: String } pub(crate) async fn approve_auth_request( client: &mut Client, input: &AuthApproveRequest, ) -> Result<()> { - let config = client.get_api_configurations().await; - // Get user reset password details let reset_password_details = bitwarden_api_api::apis::organization_users_api::organizations_org_id_users_id_reset_password_details_get( - &config.api, +&client.get_api_configurations().await.api, &input.organization_id.to_string(), &input.request_id.to_string(), ) .await?; - let encrypted_user_key = get_encrypted_user_key(client, input, reset_password_details)?; + let encrypted_user_key = get_encrypted_user_key(&client, input, reset_password_details)?; + // Need to create a new mutable borrow bitwarden_api_api::apis::organization_auth_requests_api::organizations_org_id_auth_requests_request_id_post( - &config.api, +&client.get_api_configurations().await.api, input.organization_id, input.request_id, Some(AdminAuthRequestUpdateRequestModel { @@ -70,18 +72,21 @@ fn get_encrypted_user_key( .map_err(|_| CryptoError::InvalidKey)? }; - - // Decrypt user key with decrypted org private key - // TODO - // let user_key = user_reset_password_details_res.reset_password_key - // .ok_or(Error::MissingFields)? - // .parse::()? - // .decrypt_with_key(org_private_key) - // .decrypt(enc, &Some(input.organization_id))? - // .into_bytes(); - - // Re-encrypt the User Key with the Device Public Key - // return re-encrypted user key + // Decrypt user key with org private key + let dec_user_key = org_private_key + .decrypt(Pkcs1v15Encrypt, &reset_password_details.reset_password_key.ok_or(Error::MissingFields)?.into_bytes()) + .map_err(|_| CryptoError::InvalidKey)?; // need better error + + // re-encrypt user key with device public key + let device_public_key = rsa::RsaPublicKey::from_pkcs1_der(&input.device_public_key.as_bytes()) + .map_err(|_| CryptoError::InvalidKey)?; + let mut rng = rand::thread_rng(); + let re_encrypted_user_key = device_public_key.encrypt(&mut rng, Pkcs1v15Encrypt, &dec_user_key) + .map_err(|_| CryptoError::InvalidKey)?; // need better error + + String::from_utf8(re_encrypted_user_key) + .map_err(|_| CryptoError::InvalidKey)? // need better error + .parse() } #[derive(Serialize, Deserialize, Debug, JsonSchema)] From b916bfab47c854a077e250f44fb30300ab0a9bbf Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Mon, 9 Oct 2023 09:03:50 +1000 Subject: [PATCH 04/15] Add todo --- crates/bitwarden/src/admin_console/auth_requests/approve.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden/src/admin_console/auth_requests/approve.rs b/crates/bitwarden/src/admin_console/auth_requests/approve.rs index e4d776975..5a298f422 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/approve.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/approve.rs @@ -53,6 +53,7 @@ pub(crate) async fn approve_auth_request( Ok(()) } +// TODO: most of this should be moved into crypto::rsa as encrypt/decrypt methods fn get_encrypted_user_key( client: &Client, input: &AuthApproveRequest, From b0ae9311d6fb288ecad42f5e25244a14fd6e08d1 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Mon, 23 Oct 2023 14:01:50 +1000 Subject: [PATCH 05/15] rsa decrypt --- crates/bitwarden/src/crypto/enc_string.rs | 10 +++- crates/bitwarden/src/crypto/mod.rs | 3 +- crates/bitwarden/src/crypto/rsa.rs | 56 ++++++++++++++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden/src/crypto/enc_string.rs b/crates/bitwarden/src/crypto/enc_string.rs index b701aaf8f..4996a3f1a 100644 --- a/crates/bitwarden/src/crypto/enc_string.rs +++ b/crates/bitwarden/src/crypto/enc_string.rs @@ -1,12 +1,13 @@ use std::{fmt::Display, str::FromStr}; use base64::Engine; +use rsa::RsaPrivateKey; use serde::{de::Visitor, Deserialize}; use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, - crypto::{decrypt_aes256_hmac, Decryptable, Encryptable, SymmetricCryptoKey}, + crypto::{decrypt_aes256_hmac, Decryptable, Encryptable, SymmetricCryptoKey, rsa::decrypt_rsa}, error::{CryptoError, EncStringParseError, Error, Result}, util::BASE64_ENGINE, }; @@ -315,6 +316,13 @@ impl EncString { _ => Err(CryptoError::InvalidKey.into()), } } + + pub fn decrypt_with_rsa_key(&self, key: &RsaPrivateKey) -> Result> { + match self { + EncString::Rsa2048_OaepSha1_B64 { data } => decrypt_rsa(data.clone(), key), + _ => Err(CryptoError::InvalidKey.into()), + } + } } fn invalid_len_error(expected: usize) -> impl Fn(Vec) -> EncStringParseError { diff --git a/crates/bitwarden/src/crypto/mod.rs b/crates/bitwarden/src/crypto/mod.rs index b35157981..ff21e79fb 100644 --- a/crates/bitwarden/src/crypto/mod.rs +++ b/crates/bitwarden/src/crypto/mod.rs @@ -45,10 +45,11 @@ pub(crate) use master_key::{HashPurpose, MasterKey}; mod user_key; #[cfg(feature = "internal")] pub(crate) use user_key::UserKey; -#[cfg(feature = "internal")] +// #[cfg(feature = "internal")] mod rsa; #[cfg(feature = "internal")] pub use self::rsa::RsaKeyPair; +#[cfg(feature = "internal")] #[cfg(feature = "internal")] mod fingerprint; diff --git a/crates/bitwarden/src/crypto/rsa.rs b/crates/bitwarden/src/crypto/rsa.rs index 0d2d135b9..bedfd6876 100644 --- a/crates/bitwarden/src/crypto/rsa.rs +++ b/crates/bitwarden/src/crypto/rsa.rs @@ -2,11 +2,13 @@ use base64::Engine; use rsa::{ pkcs8::{EncodePrivateKey, EncodePublicKey}, RsaPrivateKey, RsaPublicKey, + Oaep }; +use sha1::Sha1; use crate::{ crypto::{encrypt_aes256_hmac, EncString, SymmetricCryptoKey}, - error::{Error, Result}, + error::{Error, Result, CryptoError}, util::BASE64_ENGINE, }; @@ -40,3 +42,55 @@ pub(super) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { private: protected, }) } + +pub(super) fn decrypt_rsa(data: Vec, key: &RsaPrivateKey) -> Result> { + // TODO: figure out what's going on with error handling here + Ok(key.decrypt(Oaep::new::(), &data).map_err(|_| CryptoError::InvalidKey)?) +} + +#[cfg(test)] +mod tests { + use base64::Engine; + use rsa::pkcs8::DecodePrivateKey; + use crate::util::BASE64_ENGINE; + +use super::decrypt_rsa; + + #[test] + fn test_decrypt_rsa() { + let private_key_b64 = concat!( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz", + "YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L", + "nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/", + "YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK", + "PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q", + "Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj", + "WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh", + "5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk", + "1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU", + "BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf", + "TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU", + "q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv", + "q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX", + "5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1", + "eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE", + "Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8", + "+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ", + "BokBGnjFnTnKcs7nv/O8="); + + let data_b64 = concat!( + "A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV", + "4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT", + "zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D", + "/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=="); + + let private_key_bytes = BASE64_ENGINE.decode(private_key_b64).unwrap(); + let private_key = rsa::RsaPrivateKey::from_pkcs8_der(&private_key_bytes).unwrap(); + let data_bytes = BASE64_ENGINE.decode(data_b64).unwrap(); + + let result = decrypt_rsa(data_bytes, &private_key).unwrap(); + let result_string = String::from_utf8(result).unwrap(); + + assert_eq!(result_string, "EncryptMe!"); + } +} From b220dbca1e87a60123a989576af2583af324bc76 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Mon, 23 Oct 2023 14:04:11 +1000 Subject: [PATCH 06/15] Use new method in approve command --- crates/bitwarden/src/admin_console/auth_requests/approve.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden/src/admin_console/auth_requests/approve.rs b/crates/bitwarden/src/admin_console/auth_requests/approve.rs index 5a298f422..f8ff19a78 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/approve.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/approve.rs @@ -74,9 +74,8 @@ fn get_encrypted_user_key( }; // Decrypt user key with org private key - let dec_user_key = org_private_key - .decrypt(Pkcs1v15Encrypt, &reset_password_details.reset_password_key.ok_or(Error::MissingFields)?.into_bytes()) - .map_err(|_| CryptoError::InvalidKey)?; // need better error + let user_key = &reset_password_details.reset_password_key.ok_or(Error::MissingFields)?.parse::()?; + let dec_user_key = user_key.decrypt_with_rsa_key(&org_private_key)?; // re-encrypt user key with device public key let device_public_key = rsa::RsaPublicKey::from_pkcs1_der(&input.device_public_key.as_bytes()) From 11ef94373d3d2bf771fe062d785b28b2329334ba Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Mon, 23 Oct 2023 14:08:47 +1000 Subject: [PATCH 07/15] add TODO --- crates/bitwarden/src/admin_console/auth_requests/approve.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden/src/admin_console/auth_requests/approve.rs b/crates/bitwarden/src/admin_console/auth_requests/approve.rs index f8ff19a78..12e8375ca 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/approve.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/approve.rs @@ -80,6 +80,7 @@ fn get_encrypted_user_key( // re-encrypt user key with device public key let device_public_key = rsa::RsaPublicKey::from_pkcs1_der(&input.device_public_key.as_bytes()) .map_err(|_| CryptoError::InvalidKey)?; + // TODO: let re_encrypted_user_key = rsa::rsa_encrypt(dec_user_key, device_public_key) let mut rng = rand::thread_rng(); let re_encrypted_user_key = device_public_key.encrypt(&mut rng, Pkcs1v15Encrypt, &dec_user_key) .map_err(|_| CryptoError::InvalidKey)?; // need better error From 1d0ae1d0295ab70c91275cb1f2be63840c5f44c0 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 24 Oct 2023 10:42:27 +1000 Subject: [PATCH 08/15] Implement rsa_encrypt --- .../admin_console/auth_requests/approve.rs | 100 +++++++++-------- .../src/client/encryption_settings.rs | 10 +- crates/bitwarden/src/crypto/mod.rs | 2 + crates/bitwarden/src/crypto/rsa.rs | 105 +++++++++++------- 4 files changed, 122 insertions(+), 95 deletions(-) diff --git a/crates/bitwarden/src/admin_console/auth_requests/approve.rs b/crates/bitwarden/src/admin_console/auth_requests/approve.rs index 12e8375ca..c8a0c01a1 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/approve.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/approve.rs @@ -1,16 +1,22 @@ -use bitwarden_api_api::models::{PendingOrganizationAuthRequestResponseModelListResponseModel, PendingOrganizationAuthRequestResponseModel, OrganizationUserResetPasswordDetailsResponseModel, AdminAuthRequestUpdateRequestModel}; +use base64::Engine; +use bitwarden_api_api::models::{ + AdminAuthRequestUpdateRequestModel, OrganizationUserResetPasswordDetailsResponseModel, + PendingOrganizationAuthRequestResponseModel, + PendingOrganizationAuthRequestResponseModelListResponseModel, +}; use rsa::{ - pkcs8::DecodePrivateKey, - Pkcs1v15Encrypt, pkcs1::DecodeRsaPublicKey + pkcs8::{der::Decode, DecodePrivateKey, SubjectPublicKeyInfo}, + RsaPublicKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - client::Client, - error::{Result, Error, CryptoError}, - crypto::{EncString, Decryptable} + client::Client, + crypto::{encrypt_rsa, Decryptable, EncString}, + error::{CryptoError, Error, Result}, + util::BASE64_ENGINE, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -20,7 +26,7 @@ pub struct AuthApproveRequest { pub request_id: Uuid, pub organization_user_id: Uuid, pub organization_id: Uuid, - pub device_public_key: String + pub device_public_key: String, } pub(crate) async fn approve_auth_request( @@ -38,7 +44,6 @@ pub(crate) async fn approve_auth_request( let encrypted_user_key = get_encrypted_user_key(&client, input, reset_password_details)?; - // Need to create a new mutable borrow bitwarden_api_api::apis::organization_auth_requests_api::organizations_org_id_auth_requests_request_id_post( &client.get_api_configurations().await.api, input.organization_id, @@ -53,41 +58,40 @@ pub(crate) async fn approve_auth_request( Ok(()) } -// TODO: most of this should be moved into crypto::rsa as encrypt/decrypt methods fn get_encrypted_user_key( - client: &Client, - input: &AuthApproveRequest, - reset_password_details: OrganizationUserResetPasswordDetailsResponseModel) -> Result { - - // Decrypt organization's encrypted private key with org key - let enc = client.get_encryption_settings()?; - - let org_private_key = { - let dec = reset_password_details.encrypted_private_key - .ok_or(Error::MissingFields)? - .parse::()? - .decrypt(enc, &Some(input.organization_id))? - .into_bytes(); - - rsa::RsaPrivateKey::from_pkcs8_der(&dec) - .map_err(|_| CryptoError::InvalidKey)? - }; - - // Decrypt user key with org private key - let user_key = &reset_password_details.reset_password_key.ok_or(Error::MissingFields)?.parse::()?; - let dec_user_key = user_key.decrypt_with_rsa_key(&org_private_key)?; - - // re-encrypt user key with device public key - let device_public_key = rsa::RsaPublicKey::from_pkcs1_der(&input.device_public_key.as_bytes()) - .map_err(|_| CryptoError::InvalidKey)?; - // TODO: let re_encrypted_user_key = rsa::rsa_encrypt(dec_user_key, device_public_key) - let mut rng = rand::thread_rng(); - let re_encrypted_user_key = device_public_key.encrypt(&mut rng, Pkcs1v15Encrypt, &dec_user_key) - .map_err(|_| CryptoError::InvalidKey)?; // need better error - - String::from_utf8(re_encrypted_user_key) - .map_err(|_| CryptoError::InvalidKey)? // need better error - .parse() + client: &Client, + input: &AuthApproveRequest, + reset_password_details: OrganizationUserResetPasswordDetailsResponseModel, +) -> Result { + // Decrypt organization's encrypted private key with org key + let enc = client.get_encryption_settings()?; + + let org_private_key = { + let dec = reset_password_details + .encrypted_private_key + .ok_or(Error::MissingFields)? + .parse::()? + .decrypt(enc, &Some(input.organization_id))? + .into_bytes(); + + rsa::RsaPrivateKey::from_pkcs8_der(&dec).map_err(|_| CryptoError::InvalidKey)? + }; + + // Decrypt user key with org private key + let user_key = &reset_password_details + .reset_password_key + .ok_or(Error::MissingFields)? + .parse::()?; + let dec_user_key = user_key.decrypt_with_rsa_key(&org_private_key)?; + + // re-encrypt user key with device public key + let device_public_key_bytes = BASE64_ENGINE.decode(&input.device_public_key)?; + let device_public_key_info = SubjectPublicKeyInfo::from_der(&device_public_key_bytes).unwrap(); // TODO: error handling + let device_public_key = RsaPublicKey::try_from(device_public_key_info).unwrap(); // TODO: error handling + + let re_encrypted_user_key = encrypt_rsa(dec_user_key, &device_public_key)?; + + EncString::from_buffer(&re_encrypted_user_key) } #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -98,7 +102,7 @@ pub struct PendingAuthRequestsResponse { impl PendingAuthRequestsResponse { pub(crate) fn process_response( - response: PendingOrganizationAuthRequestResponseModelListResponseModel + response: PendingOrganizationAuthRequestResponseModelListResponseModel, ) -> Result { Ok(PendingAuthRequestsResponse { data: response @@ -123,13 +127,13 @@ pub struct PendingAuthRequestResponse { impl PendingAuthRequestResponse { pub(crate) fn process_response( - response: PendingOrganizationAuthRequestResponseModel + response: PendingOrganizationAuthRequestResponseModel, ) -> Result { Ok(PendingAuthRequestResponse { - id: response.id.ok_or(Error::MissingFields)?, - user_id: response.user_id.ok_or(Error::MissingFields)?, - organization_user_id: response.organization_user_id.ok_or(Error::MissingFields)?, - email: response.email.ok_or(Error::MissingFields)?, + id: response.id.ok_or(Error::MissingFields)?, + user_id: response.user_id.ok_or(Error::MissingFields)?, + organization_user_id: response.organization_user_id.ok_or(Error::MissingFields)?, + email: response.email.ok_or(Error::MissingFields)?, }) } } diff --git a/crates/bitwarden/src/client/encryption_settings.rs b/crates/bitwarden/src/client/encryption_settings.rs index 9c79a1781..58aece281 100644 --- a/crates/bitwarden/src/client/encryption_settings.rs +++ b/crates/bitwarden/src/client/encryption_settings.rs @@ -81,15 +81,7 @@ impl EncryptionSettings { // Decrypt the org keys with the private key for (org_id, org_enc_key) in org_enc_keys { - let data = match org_enc_key { - EncString::Rsa2048_OaepSha1_B64 { data } => data, - _ => return Err(CryptoError::InvalidKey.into()), - }; - - let dec = private_key - .decrypt(Oaep::new::(), &data) - .map_err(|_| CryptoError::KeyDecrypt)?; - + let dec = org_enc_key.decrypt_with_rsa_key(private_key)?; let org_key = SymmetricCryptoKey::try_from(dec.as_slice())?; self.org_keys.insert(org_id, org_key); diff --git a/crates/bitwarden/src/crypto/mod.rs b/crates/bitwarden/src/crypto/mod.rs index ff21e79fb..d27a1b33e 100644 --- a/crates/bitwarden/src/crypto/mod.rs +++ b/crates/bitwarden/src/crypto/mod.rs @@ -50,6 +50,8 @@ mod rsa; #[cfg(feature = "internal")] pub use self::rsa::RsaKeyPair; #[cfg(feature = "internal")] +pub use self::rsa::encrypt_rsa; +#[cfg(feature = "internal")] #[cfg(feature = "internal")] mod fingerprint; diff --git a/crates/bitwarden/src/crypto/rsa.rs b/crates/bitwarden/src/crypto/rsa.rs index bedfd6876..83f076450 100644 --- a/crates/bitwarden/src/crypto/rsa.rs +++ b/crates/bitwarden/src/crypto/rsa.rs @@ -44,53 +44,82 @@ pub(super) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { } pub(super) fn decrypt_rsa(data: Vec, key: &RsaPrivateKey) -> Result> { - // TODO: figure out what's going on with error handling here - Ok(key.decrypt(Oaep::new::(), &data).map_err(|_| CryptoError::InvalidKey)?) + key.decrypt(Oaep::new::(), &data) + .map_err(|_| CryptoError::InvalidKey.into()) // need better error +} + +pub fn encrypt_rsa(data: Vec, key: &RsaPublicKey) -> Result> { + let mut rng = rand::thread_rng(); + key.encrypt(&mut rng, Oaep::new::(), &data) + .map_err(|_| CryptoError::InvalidKey.into()) // need better error } #[cfg(test)] mod tests { use base64::Engine; - use rsa::pkcs8::DecodePrivateKey; + use rsa::pkcs8::{DecodePrivateKey, der::Decode, SubjectPublicKeyInfo}; use crate::util::BASE64_ENGINE; + use super::*; + + const PRIVATE_KEY_B64: &str = concat!( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz", + "YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L", + "nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/", + "YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK", + "PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q", + "Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj", + "WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh", + "5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk", + "1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU", + "BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf", + "TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU", + "q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv", + "q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX", + "5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1", + "eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE", + "Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8", + "+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ", + "BokBGnjFnTnKcs7nv/O8="); + + const PUBLIC_KEY_B64: &str = concat!( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP", + "4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP", + "RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN", + "084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc", + "xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB"); -use super::decrypt_rsa; + const DATA_B64: &str = concat!( + "A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV", + "4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT", + "zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D", + "/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=="); #[test] fn test_decrypt_rsa() { - let private_key_b64 = concat!( - "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz", - "YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L", - "nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/", - "YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK", - "PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q", - "Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj", - "WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh", - "5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk", - "1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU", - "BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf", - "TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU", - "q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv", - "q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX", - "5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1", - "eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE", - "Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8", - "+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ", - "BokBGnjFnTnKcs7nv/O8="); - - let data_b64 = concat!( - "A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV", - "4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT", - "zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D", - "/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=="); - - let private_key_bytes = BASE64_ENGINE.decode(private_key_b64).unwrap(); - let private_key = rsa::RsaPrivateKey::from_pkcs8_der(&private_key_bytes).unwrap(); - let data_bytes = BASE64_ENGINE.decode(data_b64).unwrap(); - - let result = decrypt_rsa(data_bytes, &private_key).unwrap(); - let result_string = String::from_utf8(result).unwrap(); - - assert_eq!(result_string, "EncryptMe!"); + let private_key_bytes = BASE64_ENGINE.decode(PRIVATE_KEY_B64).unwrap(); + let private_key = rsa::RsaPrivateKey::from_pkcs8_der(&private_key_bytes).unwrap(); + let data_bytes = BASE64_ENGINE.decode(DATA_B64).unwrap(); + + let result = decrypt_rsa(data_bytes, &private_key).unwrap(); + let result_string = String::from_utf8(result).unwrap(); + + assert_eq!(result_string, "EncryptMe!"); + } + + #[test] + fn test_encrypt_rsa() { + let public_key_bytes = BASE64_ENGINE.decode(PUBLIC_KEY_B64).unwrap(); + let info = SubjectPublicKeyInfo::from_der(&public_key_bytes).unwrap(); + let public_key = RsaPublicKey::try_from(info).unwrap(); + + let private_key_bytes = BASE64_ENGINE.decode(PRIVATE_KEY_B64).unwrap(); + let private_key = rsa::RsaPrivateKey::from_pkcs8_der(&private_key_bytes).unwrap(); + + let encrypted = encrypt_rsa("EncryptMe!".as_bytes().to_vec(), &public_key).unwrap(); + let decrypted = decrypt_rsa(encrypted, &private_key).unwrap(); + + let result_string = String::from_utf8(decrypted).unwrap(); + + assert_eq!(result_string, "EncryptMe!"); } } From 1f877bc758d6caa798e4c0a0ef4a5af322d6b778 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 25 Oct 2023 15:17:34 +1000 Subject: [PATCH 09/15] Wire up commands in bw --- Cargo.lock | 8 ++ .../src/admin_console/auth_requests/mod.rs | 3 + .../src/admin_console/client_auth_requests.rs | 10 +- crates/bw/Cargo.toml | 13 ++ crates/bw/src/main.rs | 34 +++++- crates/bw/src/render.rs | 114 ++++++++++++++++++ 6 files changed, 179 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bb70fdf5..a10836bd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,15 +510,23 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" name = "bw" version = "0.0.2" dependencies = [ + "bat", "bitwarden", "bitwarden-cli", + "chrono", "clap", "color-eyre", + "comfy-table", "env_logger", "inquire", "log", + "serde", + "serde_json", + "serde_yaml", + "supports-color", "tempfile", "tokio", + "uuid", ] [[package]] diff --git a/crates/bitwarden/src/admin_console/auth_requests/mod.rs b/crates/bitwarden/src/admin_console/auth_requests/mod.rs index d6e535bf7..34a7164a6 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/mod.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/mod.rs @@ -5,3 +5,6 @@ pub(crate) use list::list_pending_requests; pub use list::{ PendingAuthRequestsRequest, PendingAuthRequestsResponse, PendingAuthRequestResponse }; + +pub(crate) use approve::approve_auth_request; +pub use approve::AuthApproveRequest; diff --git a/crates/bitwarden/src/admin_console/client_auth_requests.rs b/crates/bitwarden/src/admin_console/client_auth_requests.rs index 43c29e185..8eb27e1c3 100644 --- a/crates/bitwarden/src/admin_console/client_auth_requests.rs +++ b/crates/bitwarden/src/admin_console/client_auth_requests.rs @@ -1,7 +1,8 @@ use crate::{ error::Result, Client, - admin_console::auth_requests::{PendingAuthRequestsRequest, PendingAuthRequestsResponse, list_pending_requests} + admin_console::auth_requests::{PendingAuthRequestsRequest, PendingAuthRequestsResponse, list_pending_requests}, + admin_console::auth_requests::{AuthApproveRequest, approve_auth_request} }; pub struct ClientAuthRequests<'a> { @@ -15,6 +16,13 @@ impl<'a> ClientAuthRequests<'a> { ) -> Result { list_pending_requests(self.client, input).await } + + pub async fn approve( + &mut self, + input: &AuthApproveRequest + ) -> Result<()> { + approve_auth_request(self.client, input).await + } } impl<'a> Client { diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index 0793470b3..2c012f467 100644 --- a/crates/bw/Cargo.toml +++ b/crates/bw/Cargo.toml @@ -21,6 +21,19 @@ log = "0.4.18" env_logger = "0.10.0" color-eyre = "0.6" inquire = "0.6.2" +uuid = { version = "^1.3.3", features = ["serde"] } +serde = "^1.0.163" +serde_json = "^1.0.96" +serde_yaml = "0.9" +bat = { version = "0.24.0", features = [ + "regex-onig", +], default-features = false } +chrono = { version = "0.4.26", features = [ + "clock", + "std", +], default-features = false } +comfy-table = "^7.0.1" +supports-color = "2.0.0" bitwarden = { path = "../bitwarden", version = "0.3.1", features = [ "internal", diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 73e64a4aa..76889565c 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -1,11 +1,13 @@ use bitwarden::{ auth::RegisterRequest, client::client_settings::ClientSettings, tool::PasswordGeneratorRequest, + admin_console::auth_requests::{PendingAuthRequestsRequest, AuthApproveRequest} }; use bitwarden_cli::{install_color_eyre, text_prompt_when_none, Color}; use clap::{command, Args, CommandFactory, Parser, Subcommand}; use color_eyre::eyre::Result; use inquire::Password; -use render::Output; +use render::{Output, serialize_response}; +use uuid::Uuid; mod auth; mod render; @@ -55,6 +57,12 @@ enum Commands { #[command(subcommand)] command: GeneratorCommands, }, + + #[command(long_about = "Manage your organization")] + AdminConsole { + #[command(subcommand)] + command: AdminConsoleCommands, + }, } #[derive(Args, Clone)] @@ -90,6 +98,12 @@ enum GeneratorCommands { Passphrase {}, } +#[derive(Subcommand, Clone)] +enum AdminConsoleCommands { + ListDevices { organization_id: Uuid }, + ApproveDevice { id: Uuid } +} + #[derive(Args, Clone)] struct PasswordGeneratorArgs { #[arg(short = 'l', long, action, help = "Include lowercase characters (a-z)")] @@ -182,7 +196,7 @@ async fn process_commands() -> Result<()> { } // Not login, assuming we have a config - let client = bitwarden::Client::new(None); + let mut client = bitwarden::Client::new(None); // And finally we process all the commands which require authentication match command { @@ -208,6 +222,22 @@ async fn process_commands() -> Result<()> { } GeneratorCommands::Passphrase {} => todo!(), }, + Commands::AdminConsole { command } => match command { + AdminConsoleCommands::ListDevices { organization_id } => { + let auth_requests = client + .client_auth_requests() + .list(&PendingAuthRequestsRequest { organization_id }) + .await?; + + serialize_response(auth_requests.data, cli.output, false); + }, + AdminConsoleCommands::ApproveDevice { id } => { + todo!() + // client + // .client_auth_requests() + // .approve(&AuthApproveRequest { id }) + } + } }; Ok(()) diff --git a/crates/bw/src/render.rs b/crates/bw/src/render.rs index da8ed4997..061e7559b 100644 --- a/crates/bw/src/render.rs +++ b/crates/bw/src/render.rs @@ -1,4 +1,8 @@ +use bitwarden::admin_console::auth_requests::{PendingAuthRequestsResponse, PendingAuthRequestResponse}; +use chrono::{DateTime, Utc}; use clap::ValueEnum; +use comfy_table::Table; +use serde::Serialize; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] #[allow(clippy::upper_case_acronyms)] @@ -9,3 +13,113 @@ pub(crate) enum Output { TSV, None, } + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +pub(crate) enum Color { + No, + Yes, + Auto, +} + +impl Color { + pub(crate) fn is_enabled(self) -> bool { + match self { + Color::No => false, + Color::Yes => true, + Color::Auto => supports_color::on(supports_color::Stream::Stdout).is_some(), + } + } +} + +const ASCII_HEADER_ONLY: &str = " -- "; + +pub(crate) fn serialize_response, const N: usize>( + data: T, + output: Output, + color: bool, +) { + match output { + Output::JSON => { + let mut text = serde_json::to_string_pretty(&data).unwrap(); + // Yaml/table/tsv serializations add a newline at the end, so we do the same here for consistency + text.push('\n'); + pretty_print("json", &text, color); + } + Output::YAML => { + let text = serde_yaml::to_string(&data).unwrap(); + pretty_print("yaml", &text, color); + } + Output::Table => { + let mut table = Table::new(); + table + .load_preset(ASCII_HEADER_ONLY) + .set_header(T::get_headers()) + .add_rows(data.get_values()); + + println!("{table}"); + } + Output::TSV => { + println!("{}", T::get_headers().join("\t")); + + let rows: Vec = data + .get_values() + .into_iter() + .map(|row| row.join("\t")) + .collect(); + println!("{}", rows.join("\n")); + } + Output::None => {} + } +} + +fn pretty_print(language: &str, data: &str, color: bool) { + if color { + bat::PrettyPrinter::new() + .input_from_bytes(data.as_bytes()) + .language(language) + .print() + .unwrap(); + } else { + print!("{}", data); + } +} + +// We're using const generics for the array lengths to make sure the header count and value count match +pub(crate) trait TableSerialize: Sized { + fn get_headers() -> [&'static str; N]; + fn get_values(&self) -> Vec<[String; N]>; +} + +// Generic impl for Vec so we can call `serialize_response` with both individual +// elements and lists of elements, like we do with the JSON and YAML cases +impl, const N: usize> TableSerialize for Vec { + fn get_headers() -> [&'static str; N] { + T::get_headers() + } + fn get_values(&self) -> Vec<[String; N]> { + let mut values = Vec::new(); + for t in self { + values.append(&mut t.get_values()); + } + values + } +} + +fn format_date(date: &DateTime) -> String { + date.format("%Y-%m-%d %H:%M:%S").to_string() +} + +impl TableSerialize<4> for PendingAuthRequestResponse { + fn get_headers() -> [&'static str; 4] { + ["ID", "User ID", "Organization User ID", "Email"] + } + + fn get_values(&self) -> Vec<[String; 4]> { + vec![[ + self.id.to_string(), + self.user_id.to_string(), + self.organization_user_id.to_string(), + self.email.clone() + ]] + } +} From e6270bc2a8e3082a16ead0948dbe853ef9be6117 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 25 Oct 2023 15:39:14 +1000 Subject: [PATCH 10/15] FIXME: terrible hack login method --- crates/bw/src/auth/login.rs | 4 ++-- crates/bw/src/main.rs | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index 1c169817f..dd1e7af3a 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -84,7 +84,7 @@ pub(crate) async fn api_key_login( mut client: Client, client_id: Option, client_secret: Option, -) -> Result<()> { +) -> Result { let client_id = text_prompt_when_none("Client ID", client_id)?; let client_secret = text_prompt_when_none("Client Secret", client_secret)?; @@ -100,5 +100,5 @@ pub(crate) async fn api_key_login( debug!("{:?}", result); - Ok(()) + Ok(client) } diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 76889565c..af49591c3 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -162,7 +162,9 @@ async fn process_commands() -> Result<()> { LoginCommands::ApiKey { client_id, client_secret, - } => auth::api_key_login(client, client_id, client_secret).await?, + } => { + auth::api_key_login(client, client_id, client_secret).await?; + } } return Ok(()); } @@ -224,6 +226,19 @@ async fn process_commands() -> Result<()> { }, Commands::AdminConsole { command } => match command { AdminConsoleCommands::ListDevices { organization_id } => { + + // hack login + let server = "https://vault.qa.bitwarden.pw"; + let settings = ClientSettings { + api_url: format!("{}/api", server), + identity_url: format!("{}/identity", server), + ..Default::default() + }; + let client = bitwarden::Client::new(Some(settings)); + + let mut client = auth::api_key_login(client, None, None).await?; + + // continue let auth_requests = client .client_auth_requests() .list(&PendingAuthRequestsRequest { organization_id }) From feaeb68d6380ba527af87372354ea8595e5ee59f Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Thu, 26 Oct 2023 12:21:05 +1000 Subject: [PATCH 11/15] Map rest of list response model fields --- .../src/admin_console/auth_requests/list.rs | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/bitwarden/src/admin_console/auth_requests/list.rs b/crates/bitwarden/src/admin_console/auth_requests/list.rs index 587473f4f..ba152b885 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/list.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/list.rs @@ -1,11 +1,15 @@ -use bitwarden_api_api::models::{PendingOrganizationAuthRequestResponseModelListResponseModel, PendingOrganizationAuthRequestResponseModel}; +use bitwarden_api_api::models::{ + PendingOrganizationAuthRequestResponseModel, + PendingOrganizationAuthRequestResponseModelListResponseModel, +}; +use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - client::Client, - error::{Result, Error} + client::Client, + error::{Error, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -37,7 +41,7 @@ pub struct PendingAuthRequestsResponse { impl PendingAuthRequestsResponse { pub(crate) fn process_response( - response: PendingOrganizationAuthRequestResponseModelListResponseModel + response: PendingOrganizationAuthRequestResponseModelListResponseModel, ) -> Result { Ok(PendingAuthRequestsResponse { data: response @@ -57,18 +61,32 @@ pub struct PendingAuthRequestResponse { pub user_id: Uuid, pub organization_user_id: Uuid, pub email: String, - // TODO: map rest of fields + pub public_key_b64: String, + pub request_device_identifier: String, + pub request_device_type: String, + pub request_ip_address: String, + pub creation_date: DateTime, } impl PendingAuthRequestResponse { pub(crate) fn process_response( - response: PendingOrganizationAuthRequestResponseModel - ) -> Result { + response: PendingOrganizationAuthRequestResponseModel, + ) -> Result { Ok(PendingAuthRequestResponse { - id: response.id.ok_or(Error::MissingFields)?, - user_id: response.user_id.ok_or(Error::MissingFields)?, - organization_user_id: response.organization_user_id.ok_or(Error::MissingFields)?, - email: response.email.ok_or(Error::MissingFields)?, + id: response.id.ok_or(Error::MissingFields)?, + user_id: response.user_id.ok_or(Error::MissingFields)?, + organization_user_id: response.organization_user_id.ok_or(Error::MissingFields)?, + email: response.email.ok_or(Error::MissingFields)?, + public_key_b64: response.public_key.ok_or(Error::MissingFields)?, + request_device_identifier: response + .request_device_identifier + .ok_or(Error::MissingFields)?, + request_device_type: response.request_device_type.ok_or(Error::MissingFields)?, + request_ip_address: response.request_ip_address.ok_or(Error::MissingFields)?, + creation_date: response + .creation_date + .ok_or(Error::MissingFields)? + .parse()?, }) } } From e767bdd0b1bb204cfb3453b46f9349c357c618b0 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Thu, 26 Oct 2023 12:21:48 +1000 Subject: [PATCH 12/15] Move pub priv key functions to rsa module --- crates/bitwarden/src/crypto/mod.rs | 4 ++++ crates/bitwarden/src/crypto/rsa.rs | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden/src/crypto/mod.rs b/crates/bitwarden/src/crypto/mod.rs index d27a1b33e..a281d4cc4 100644 --- a/crates/bitwarden/src/crypto/mod.rs +++ b/crates/bitwarden/src/crypto/mod.rs @@ -52,6 +52,10 @@ pub use self::rsa::RsaKeyPair; #[cfg(feature = "internal")] pub use self::rsa::encrypt_rsa; #[cfg(feature = "internal")] +pub use self::rsa::public_key_from_b64; +#[cfg(feature = "internal")] +pub use self::rsa::private_key_from_bytes; +#[cfg(feature = "internal")] #[cfg(feature = "internal")] mod fingerprint; diff --git a/crates/bitwarden/src/crypto/rsa.rs b/crates/bitwarden/src/crypto/rsa.rs index 83f076450..6c1ed8fba 100644 --- a/crates/bitwarden/src/crypto/rsa.rs +++ b/crates/bitwarden/src/crypto/rsa.rs @@ -1,6 +1,6 @@ use base64::Engine; use rsa::{ - pkcs8::{EncodePrivateKey, EncodePublicKey}, + pkcs8::{der::Decode, EncodePrivateKey, EncodePublicKey, SubjectPublicKeyInfo, DecodePrivateKey}, RsaPrivateKey, RsaPublicKey, Oaep }; @@ -54,6 +54,16 @@ pub fn encrypt_rsa(data: Vec, key: &RsaPublicKey) -> Result> { .map_err(|_| CryptoError::InvalidKey.into()) // need better error } +pub fn public_key_from_b64(b64: &str) -> Result { + let public_key_bytes = BASE64_ENGINE.decode(b64)?; + let public_key_info = SubjectPublicKeyInfo::from_der(&public_key_bytes).unwrap(); // TODO: error handling + RsaPublicKey::try_from(public_key_info).map_err(|_| Error::Crypto(CryptoError::InvalidKey)) +} + +pub fn private_key_from_bytes(bytes: &Vec) -> Result { + rsa::RsaPrivateKey::from_pkcs8_der(bytes).map_err(|_| Error::Crypto(CryptoError::InvalidKey)) +} + #[cfg(test)] mod tests { use base64::Engine; From a1010d44be76c0e0357bb45433bfb76557be430e Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Thu, 26 Oct 2023 12:22:22 +1000 Subject: [PATCH 13/15] WIP approve --- .../admin_console/auth_requests/approve.rs | 108 +++++++----------- crates/bw/src/main.rs | 43 +++---- 2 files changed, 64 insertions(+), 87 deletions(-) diff --git a/crates/bitwarden/src/admin_console/auth_requests/approve.rs b/crates/bitwarden/src/admin_console/auth_requests/approve.rs index c8a0c01a1..f594c9963 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/approve.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/approve.rs @@ -1,12 +1,5 @@ -use base64::Engine; use bitwarden_api_api::models::{ AdminAuthRequestUpdateRequestModel, OrganizationUserResetPasswordDetailsResponseModel, - PendingOrganizationAuthRequestResponseModel, - PendingOrganizationAuthRequestResponseModelListResponseModel, -}; -use rsa::{ - pkcs8::{der::Decode, DecodePrivateKey, SubjectPublicKeyInfo}, - RsaPublicKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -14,40 +7,47 @@ use uuid::Uuid; use crate::{ client::Client, - crypto::{encrypt_rsa, Decryptable, EncString}, - error::{CryptoError, Error, Result}, - util::BASE64_ENGINE, + crypto::{encrypt_rsa, private_key_from_bytes, public_key_from_b64, Decryptable, EncString}, + error::{Error, Result}, }; +use super::{list_pending_requests, PendingAuthRequestResponse}; + +// TODO: what identifier should this take? e.g. org_user_id, request_id, etc +// using org_user_id for now #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AuthApproveRequest { - /// ID of the auth request to approve - pub request_id: Uuid, pub organization_user_id: Uuid, pub organization_id: Uuid, - pub device_public_key: String, } pub(crate) async fn approve_auth_request( client: &mut Client, input: &AuthApproveRequest, ) -> Result<()> { + let device_request = get_pending_request(input.organization_id, input.organization_user_id, client).await; + // Get user reset password details let reset_password_details = bitwarden_api_api::apis::organization_users_api::organizations_org_id_users_id_reset_password_details_get( &client.get_api_configurations().await.api, &input.organization_id.to_string(), - &input.request_id.to_string(), + &device_request.id.to_string(), ) .await?; - let encrypted_user_key = get_encrypted_user_key(&client, input, reset_password_details)?; + let encrypted_user_key = get_encrypted_user_key( + &client, + input.organization_id, + &device_request, + reset_password_details, + )?; bitwarden_api_api::apis::organization_auth_requests_api::organizations_org_id_auth_requests_request_id_post( &client.get_api_configurations().await.api, input.organization_id, - input.request_id, + device_request.id, Some(AdminAuthRequestUpdateRequestModel { encrypted_user_key: Some(encrypted_user_key.to_string()), request_approved: true @@ -58,9 +58,30 @@ pub(crate) async fn approve_auth_request( Ok(()) } +async fn get_pending_request(organization_id: Uuid, organization_user_id: Uuid, client: &mut Client) -> PendingAuthRequestResponse { + // hack: get all approval details and then find the one we want + // when we settle on an identifier then we should just give ourselves a better server API + // or do we require the caller to pass all this info in? + let all_device_requests = list_pending_requests( + client, + &super::PendingAuthRequestsRequest { + organization_id: organization_id, + }, + ) + .await; + + all_device_requests + .unwrap() + .data + .into_iter() + .find(|r| r.organization_user_id == organization_user_id) + .unwrap() // TODO: error handling +} + fn get_encrypted_user_key( client: &Client, - input: &AuthApproveRequest, + organization_id: Uuid, + input: &PendingAuthRequestResponse, reset_password_details: OrganizationUserResetPasswordDetailsResponseModel, ) -> Result { // Decrypt organization's encrypted private key with org key @@ -71,10 +92,10 @@ fn get_encrypted_user_key( .encrypted_private_key .ok_or(Error::MissingFields)? .parse::()? - .decrypt(enc, &Some(input.organization_id))? + .decrypt(enc, &Some(organization_id))? .into_bytes(); - rsa::RsaPrivateKey::from_pkcs8_der(&dec).map_err(|_| CryptoError::InvalidKey)? + private_key_from_bytes(&dec)? }; // Decrypt user key with org private key @@ -85,55 +106,8 @@ fn get_encrypted_user_key( let dec_user_key = user_key.decrypt_with_rsa_key(&org_private_key)?; // re-encrypt user key with device public key - let device_public_key_bytes = BASE64_ENGINE.decode(&input.device_public_key)?; - let device_public_key_info = SubjectPublicKeyInfo::from_der(&device_public_key_bytes).unwrap(); // TODO: error handling - let device_public_key = RsaPublicKey::try_from(device_public_key_info).unwrap(); // TODO: error handling - + let device_public_key = public_key_from_b64(&input.public_key_b64)?; let re_encrypted_user_key = encrypt_rsa(dec_user_key, &device_public_key)?; EncString::from_buffer(&re_encrypted_user_key) } - -#[derive(Serialize, Deserialize, Debug, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct PendingAuthRequestsResponse { - pub data: Vec, -} - -impl PendingAuthRequestsResponse { - pub(crate) fn process_response( - response: PendingOrganizationAuthRequestResponseModelListResponseModel, - ) -> Result { - Ok(PendingAuthRequestsResponse { - data: response - .data - .unwrap_or_default() - .into_iter() - .map(|r| PendingAuthRequestResponse::process_response(r)) - .collect::>()?, - }) - } -} - -#[derive(Serialize, Deserialize, Debug, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct PendingAuthRequestResponse { - pub id: Uuid, - pub user_id: Uuid, - pub organization_user_id: Uuid, - pub email: String, - // TODO: map rest of fields -} - -impl PendingAuthRequestResponse { - pub(crate) fn process_response( - response: PendingOrganizationAuthRequestResponseModel, - ) -> Result { - Ok(PendingAuthRequestResponse { - id: response.id.ok_or(Error::MissingFields)?, - user_id: response.user_id.ok_or(Error::MissingFields)?, - organization_user_id: response.organization_user_id.ok_or(Error::MissingFields)?, - email: response.email.ok_or(Error::MissingFields)?, - }) - } -} diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index af49591c3..a200cbd49 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -1,6 +1,6 @@ use bitwarden::{ auth::RegisterRequest, client::client_settings::ClientSettings, tool::PasswordGeneratorRequest, - admin_console::auth_requests::{PendingAuthRequestsRequest, AuthApproveRequest} + admin_console::auth_requests::{PendingAuthRequestsRequest, AuthApproveRequest}, Client }; use bitwarden_cli::{install_color_eyre, text_prompt_when_none, Color}; use clap::{command, Args, CommandFactory, Parser, Subcommand}; @@ -101,7 +101,7 @@ enum GeneratorCommands { #[derive(Subcommand, Clone)] enum AdminConsoleCommands { ListDevices { organization_id: Uuid }, - ApproveDevice { id: Uuid } + ApproveDevice { organization_id: Uuid, organization_user_id: Uuid } } #[derive(Args, Clone)] @@ -134,6 +134,19 @@ async fn main() -> Result<()> { process_commands().await } +async fn hack_login() -> Client { + // hack login + let server = "https://vault.qa.bitwarden.pw"; + let settings = ClientSettings { + api_url: format!("{}/api", server), + identity_url: format!("{}/identity", server), + ..Default::default() + }; + let client = bitwarden::Client::new(Some(settings)); + + auth::api_key_login(client, None, None).await.unwrap() +} + async fn process_commands() -> Result<()> { let cli = Cli::parse(); @@ -226,19 +239,7 @@ async fn process_commands() -> Result<()> { }, Commands::AdminConsole { command } => match command { AdminConsoleCommands::ListDevices { organization_id } => { - - // hack login - let server = "https://vault.qa.bitwarden.pw"; - let settings = ClientSettings { - api_url: format!("{}/api", server), - identity_url: format!("{}/identity", server), - ..Default::default() - }; - let client = bitwarden::Client::new(Some(settings)); - - let mut client = auth::api_key_login(client, None, None).await?; - - // continue + let mut client = hack_login().await; let auth_requests = client .client_auth_requests() .list(&PendingAuthRequestsRequest { organization_id }) @@ -246,11 +247,13 @@ async fn process_commands() -> Result<()> { serialize_response(auth_requests.data, cli.output, false); }, - AdminConsoleCommands::ApproveDevice { id } => { - todo!() - // client - // .client_auth_requests() - // .approve(&AuthApproveRequest { id }) + AdminConsoleCommands::ApproveDevice { organization_id, organization_user_id } => { + let mut client = hack_login().await; + client + .client_auth_requests() + .approve(&AuthApproveRequest { organization_id, organization_user_id }) + .await + .unwrap(); // error handling? } } }; From 6058e320bf7546e71c56b37080c989e6f385224f Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Thu, 26 Oct 2023 13:19:43 +1000 Subject: [PATCH 14/15] cargo fmt --- .../admin_console/auth_requests/approve.rs | 9 ++- .../src/admin_console/auth_requests/mod.rs | 4 +- .../src/admin_console/client_auth_requests.rs | 13 ++-- crates/bitwarden/src/crypto/enc_string.rs | 2 +- crates/bitwarden/src/crypto/mod.rs | 7 +- crates/bitwarden/src/crypto/rsa.rs | 77 ++++++++++--------- crates/bitwarden/src/lib.rs | 2 +- crates/bw/src/main.rs | 58 ++++++++------ crates/bw/src/render.rs | 6 +- 9 files changed, 99 insertions(+), 79 deletions(-) diff --git a/crates/bitwarden/src/admin_console/auth_requests/approve.rs b/crates/bitwarden/src/admin_console/auth_requests/approve.rs index f594c9963..4776bb22a 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/approve.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/approve.rs @@ -26,7 +26,8 @@ pub(crate) async fn approve_auth_request( client: &mut Client, input: &AuthApproveRequest, ) -> Result<()> { - let device_request = get_pending_request(input.organization_id, input.organization_user_id, client).await; + let device_request = + get_pending_request(input.organization_id, input.organization_user_id, client).await; // Get user reset password details let reset_password_details = @@ -58,7 +59,11 @@ pub(crate) async fn approve_auth_request( Ok(()) } -async fn get_pending_request(organization_id: Uuid, organization_user_id: Uuid, client: &mut Client) -> PendingAuthRequestResponse { +async fn get_pending_request( + organization_id: Uuid, + organization_user_id: Uuid, + client: &mut Client, +) -> PendingAuthRequestResponse { // hack: get all approval details and then find the one we want // when we settle on an identifier then we should just give ourselves a better server API // or do we require the caller to pass all this info in? diff --git a/crates/bitwarden/src/admin_console/auth_requests/mod.rs b/crates/bitwarden/src/admin_console/auth_requests/mod.rs index 34a7164a6..879559e31 100644 --- a/crates/bitwarden/src/admin_console/auth_requests/mod.rs +++ b/crates/bitwarden/src/admin_console/auth_requests/mod.rs @@ -1,9 +1,9 @@ -mod list; mod approve; +mod list; pub(crate) use list::list_pending_requests; pub use list::{ - PendingAuthRequestsRequest, PendingAuthRequestsResponse, PendingAuthRequestResponse + PendingAuthRequestResponse, PendingAuthRequestsRequest, PendingAuthRequestsResponse, }; pub(crate) use approve::approve_auth_request; diff --git a/crates/bitwarden/src/admin_console/client_auth_requests.rs b/crates/bitwarden/src/admin_console/client_auth_requests.rs index 8eb27e1c3..37f032e79 100644 --- a/crates/bitwarden/src/admin_console/client_auth_requests.rs +++ b/crates/bitwarden/src/admin_console/client_auth_requests.rs @@ -1,8 +1,10 @@ use crate::{ + admin_console::auth_requests::{approve_auth_request, AuthApproveRequest}, + admin_console::auth_requests::{ + list_pending_requests, PendingAuthRequestsRequest, PendingAuthRequestsResponse, + }, error::Result, Client, - admin_console::auth_requests::{PendingAuthRequestsRequest, PendingAuthRequestsResponse, list_pending_requests}, - admin_console::auth_requests::{AuthApproveRequest, approve_auth_request} }; pub struct ClientAuthRequests<'a> { @@ -17,11 +19,8 @@ impl<'a> ClientAuthRequests<'a> { list_pending_requests(self.client, input).await } - pub async fn approve( - &mut self, - input: &AuthApproveRequest - ) -> Result<()> { - approve_auth_request(self.client, input).await + pub async fn approve(&mut self, input: &AuthApproveRequest) -> Result<()> { + approve_auth_request(self.client, input).await } } diff --git a/crates/bitwarden/src/crypto/enc_string.rs b/crates/bitwarden/src/crypto/enc_string.rs index 4996a3f1a..b55304efc 100644 --- a/crates/bitwarden/src/crypto/enc_string.rs +++ b/crates/bitwarden/src/crypto/enc_string.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, - crypto::{decrypt_aes256_hmac, Decryptable, Encryptable, SymmetricCryptoKey, rsa::decrypt_rsa}, + crypto::{decrypt_aes256_hmac, rsa::decrypt_rsa, Decryptable, Encryptable, SymmetricCryptoKey}, error::{CryptoError, EncStringParseError, Error, Result}, util::BASE64_ENGINE, }; diff --git a/crates/bitwarden/src/crypto/mod.rs b/crates/bitwarden/src/crypto/mod.rs index a281d4cc4..f2ff2f614 100644 --- a/crates/bitwarden/src/crypto/mod.rs +++ b/crates/bitwarden/src/crypto/mod.rs @@ -48,15 +48,14 @@ pub(crate) use user_key::UserKey; // #[cfg(feature = "internal")] mod rsa; #[cfg(feature = "internal")] -pub use self::rsa::RsaKeyPair; -#[cfg(feature = "internal")] pub use self::rsa::encrypt_rsa; #[cfg(feature = "internal")] +pub use self::rsa::private_key_from_bytes; +#[cfg(feature = "internal")] pub use self::rsa::public_key_from_b64; #[cfg(feature = "internal")] -pub use self::rsa::private_key_from_bytes; +pub use self::rsa::RsaKeyPair; #[cfg(feature = "internal")] - #[cfg(feature = "internal")] mod fingerprint; #[cfg(feature = "internal")] diff --git a/crates/bitwarden/src/crypto/rsa.rs b/crates/bitwarden/src/crypto/rsa.rs index 6c1ed8fba..95c158ad1 100644 --- a/crates/bitwarden/src/crypto/rsa.rs +++ b/crates/bitwarden/src/crypto/rsa.rs @@ -1,14 +1,15 @@ use base64::Engine; use rsa::{ - pkcs8::{der::Decode, EncodePrivateKey, EncodePublicKey, SubjectPublicKeyInfo, DecodePrivateKey}, - RsaPrivateKey, RsaPublicKey, - Oaep + pkcs8::{ + der::Decode, DecodePrivateKey, EncodePrivateKey, EncodePublicKey, SubjectPublicKeyInfo, + }, + Oaep, RsaPrivateKey, RsaPublicKey, }; use sha1::Sha1; use crate::{ crypto::{encrypt_aes256_hmac, EncString, SymmetricCryptoKey}, - error::{Error, Result, CryptoError}, + error::{CryptoError, Error, Result}, util::BASE64_ENGINE, }; @@ -44,14 +45,14 @@ pub(super) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { } pub(super) fn decrypt_rsa(data: Vec, key: &RsaPrivateKey) -> Result> { - key.decrypt(Oaep::new::(), &data) - .map_err(|_| CryptoError::InvalidKey.into()) // need better error + key.decrypt(Oaep::new::(), &data) + .map_err(|_| CryptoError::InvalidKey.into()) // need better error } pub fn encrypt_rsa(data: Vec, key: &RsaPublicKey) -> Result> { - let mut rng = rand::thread_rng(); - key.encrypt(&mut rng, Oaep::new::(), &data) - .map_err(|_| CryptoError::InvalidKey.into()) // need better error + let mut rng = rand::thread_rng(); + key.encrypt(&mut rng, Oaep::new::(), &data) + .map_err(|_| CryptoError::InvalidKey.into()) // need better error } pub fn public_key_from_b64(b64: &str) -> Result { @@ -61,17 +62,17 @@ pub fn public_key_from_b64(b64: &str) -> Result { } pub fn private_key_from_bytes(bytes: &Vec) -> Result { - rsa::RsaPrivateKey::from_pkcs8_der(bytes).map_err(|_| Error::Crypto(CryptoError::InvalidKey)) + rsa::RsaPrivateKey::from_pkcs8_der(bytes).map_err(|_| Error::Crypto(CryptoError::InvalidKey)) } #[cfg(test)] mod tests { - use base64::Engine; - use rsa::pkcs8::{DecodePrivateKey, der::Decode, SubjectPublicKeyInfo}; - use crate::util::BASE64_ENGINE; - use super::*; + use super::*; + use crate::util::BASE64_ENGINE; + use base64::Engine; + use rsa::pkcs8::{der::Decode, DecodePrivateKey, SubjectPublicKeyInfo}; - const PRIVATE_KEY_B64: &str = concat!( + const PRIVATE_KEY_B64: &str = concat!( "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz", "YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L", "nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/", @@ -91,45 +92,45 @@ mod tests { "+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ", "BokBGnjFnTnKcs7nv/O8="); - const PUBLIC_KEY_B64: &str = concat!( + const PUBLIC_KEY_B64: &str = concat!( "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP", "4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP", "RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN", "084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc", "xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB"); - const DATA_B64: &str = concat!( + const DATA_B64: &str = concat!( "A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV", "4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT", "zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D", "/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=="); - #[test] - fn test_decrypt_rsa() { - let private_key_bytes = BASE64_ENGINE.decode(PRIVATE_KEY_B64).unwrap(); - let private_key = rsa::RsaPrivateKey::from_pkcs8_der(&private_key_bytes).unwrap(); - let data_bytes = BASE64_ENGINE.decode(DATA_B64).unwrap(); + #[test] + fn test_decrypt_rsa() { + let private_key_bytes = BASE64_ENGINE.decode(PRIVATE_KEY_B64).unwrap(); + let private_key = rsa::RsaPrivateKey::from_pkcs8_der(&private_key_bytes).unwrap(); + let data_bytes = BASE64_ENGINE.decode(DATA_B64).unwrap(); - let result = decrypt_rsa(data_bytes, &private_key).unwrap(); - let result_string = String::from_utf8(result).unwrap(); + let result = decrypt_rsa(data_bytes, &private_key).unwrap(); + let result_string = String::from_utf8(result).unwrap(); - assert_eq!(result_string, "EncryptMe!"); - } + assert_eq!(result_string, "EncryptMe!"); + } - #[test] - fn test_encrypt_rsa() { - let public_key_bytes = BASE64_ENGINE.decode(PUBLIC_KEY_B64).unwrap(); - let info = SubjectPublicKeyInfo::from_der(&public_key_bytes).unwrap(); - let public_key = RsaPublicKey::try_from(info).unwrap(); + #[test] + fn test_encrypt_rsa() { + let public_key_bytes = BASE64_ENGINE.decode(PUBLIC_KEY_B64).unwrap(); + let info = SubjectPublicKeyInfo::from_der(&public_key_bytes).unwrap(); + let public_key = RsaPublicKey::try_from(info).unwrap(); - let private_key_bytes = BASE64_ENGINE.decode(PRIVATE_KEY_B64).unwrap(); - let private_key = rsa::RsaPrivateKey::from_pkcs8_der(&private_key_bytes).unwrap(); + let private_key_bytes = BASE64_ENGINE.decode(PRIVATE_KEY_B64).unwrap(); + let private_key = rsa::RsaPrivateKey::from_pkcs8_der(&private_key_bytes).unwrap(); - let encrypted = encrypt_rsa("EncryptMe!".as_bytes().to_vec(), &public_key).unwrap(); - let decrypted = decrypt_rsa(encrypted, &private_key).unwrap(); + let encrypted = encrypt_rsa("EncryptMe!".as_bytes().to_vec(), &public_key).unwrap(); + let decrypted = decrypt_rsa(encrypted, &private_key).unwrap(); - let result_string = String::from_utf8(decrypted).unwrap(); + let result_string = String::from_utf8(decrypted).unwrap(); - assert_eq!(result_string, "EncryptMe!"); - } + assert_eq!(result_string, "EncryptMe!"); + } } diff --git a/crates/bitwarden/src/lib.rs b/crates/bitwarden/src/lib.rs index 116722b85..1caf754af 100644 --- a/crates/bitwarden/src/lib.rs +++ b/crates/bitwarden/src/lib.rs @@ -51,6 +51,7 @@ #[cfg(feature = "mobile")] uniffi::setup_scaffolding!(); +pub mod admin_console; pub mod auth; pub mod client; pub mod crypto; @@ -69,7 +70,6 @@ mod util; #[cfg(feature = "mobile")] pub mod vault; pub mod wordlist; -pub mod admin_console; pub use client::Client; diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index a200cbd49..4fb5cb2fb 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -1,12 +1,15 @@ use bitwarden::{ - auth::RegisterRequest, client::client_settings::ClientSettings, tool::PasswordGeneratorRequest, - admin_console::auth_requests::{PendingAuthRequestsRequest, AuthApproveRequest}, Client + admin_console::auth_requests::{AuthApproveRequest, PendingAuthRequestsRequest}, + auth::RegisterRequest, + client::client_settings::ClientSettings, + tool::PasswordGeneratorRequest, + Client, }; use bitwarden_cli::{install_color_eyre, text_prompt_when_none, Color}; use clap::{command, Args, CommandFactory, Parser, Subcommand}; use color_eyre::eyre::Result; use inquire::Password; -use render::{Output, serialize_response}; +use render::{serialize_response, Output}; use uuid::Uuid; mod auth; @@ -100,8 +103,13 @@ enum GeneratorCommands { #[derive(Subcommand, Clone)] enum AdminConsoleCommands { - ListDevices { organization_id: Uuid }, - ApproveDevice { organization_id: Uuid, organization_user_id: Uuid } + ListDevices { + organization_id: Uuid, + }, + ApproveDevice { + organization_id: Uuid, + organization_user_id: Uuid, + }, } #[derive(Args, Clone)] @@ -176,7 +184,7 @@ async fn process_commands() -> Result<()> { client_id, client_secret, } => { - auth::api_key_login(client, client_id, client_secret).await?; + auth::api_key_login(client, client_id, client_secret).await?; } } return Ok(()); @@ -238,24 +246,30 @@ async fn process_commands() -> Result<()> { GeneratorCommands::Passphrase {} => todo!(), }, Commands::AdminConsole { command } => match command { - AdminConsoleCommands::ListDevices { organization_id } => { - let mut client = hack_login().await; - let auth_requests = client - .client_auth_requests() - .list(&PendingAuthRequestsRequest { organization_id }) - .await?; + AdminConsoleCommands::ListDevices { organization_id } => { + let mut client = hack_login().await; + let auth_requests = client + .client_auth_requests() + .list(&PendingAuthRequestsRequest { organization_id }) + .await?; serialize_response(auth_requests.data, cli.output, false); - }, - AdminConsoleCommands::ApproveDevice { organization_id, organization_user_id } => { - let mut client = hack_login().await; - client - .client_auth_requests() - .approve(&AuthApproveRequest { organization_id, organization_user_id }) - .await - .unwrap(); // error handling? - } - } + } + AdminConsoleCommands::ApproveDevice { + organization_id, + organization_user_id, + } => { + let mut client = hack_login().await; + client + .client_auth_requests() + .approve(&AuthApproveRequest { + organization_id, + organization_user_id, + }) + .await + .unwrap(); // error handling? + } + }, }; Ok(()) diff --git a/crates/bw/src/render.rs b/crates/bw/src/render.rs index 061e7559b..6baa64d42 100644 --- a/crates/bw/src/render.rs +++ b/crates/bw/src/render.rs @@ -1,4 +1,6 @@ -use bitwarden::admin_console::auth_requests::{PendingAuthRequestsResponse, PendingAuthRequestResponse}; +use bitwarden::admin_console::auth_requests::{ + PendingAuthRequestResponse, PendingAuthRequestsResponse, +}; use chrono::{DateTime, Utc}; use clap::ValueEnum; use comfy_table::Table; @@ -119,7 +121,7 @@ impl TableSerialize<4> for PendingAuthRequestResponse { self.id.to_string(), self.user_id.to_string(), self.organization_user_id.to_string(), - self.email.clone() + self.email.clone(), ]] } } From d7e1a5cb66416e356cc03b49178b3e9d59cc6709 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Thu, 26 Oct 2023 13:20:38 +1000 Subject: [PATCH 15/15] Fix typo --- crates/bitwarden/src/crypto/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden/src/crypto/mod.rs b/crates/bitwarden/src/crypto/mod.rs index f2ff2f614..0e6fb1f16 100644 --- a/crates/bitwarden/src/crypto/mod.rs +++ b/crates/bitwarden/src/crypto/mod.rs @@ -56,7 +56,6 @@ pub use self::rsa::public_key_from_b64; #[cfg(feature = "internal")] pub use self::rsa::RsaKeyPair; #[cfg(feature = "internal")] -#[cfg(feature = "internal")] mod fingerprint; #[cfg(feature = "internal")] pub(crate) use fingerprint::fingerprint;