// serde_repr generates some as conversions that we can't seem to silence from // here, unfortunately #![allow(clippy::as_conversions)] use crate::prelude::*; use crate::json::{ DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _, }; use tokio::io::AsyncReadExt as _; #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Copy, Clone, PartialEq, Eq, )] #[repr(u8)] pub enum UriMatchType { Domain = 0, Host = 1, StartsWith = 2, Exact = 3, RegularExpression = 4, Never = 5, } impl std::fmt::Display for UriMatchType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { #[allow(clippy::enum_glob_use)] use UriMatchType::*; let s = match self { Domain => "domain", Host => "host", StartsWith => "starts_with", Exact => "exact", RegularExpression => "regular_expression", Never => "never", }; write!(f, "{s}") } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TwoFactorProviderType { Authenticator = 0, Email = 1, Duo = 2, Yubikey = 3, U2f = 4, Remember = 5, OrganizationDuo = 6, WebAuthn = 7, } impl TwoFactorProviderType { #[must_use] pub fn message(&self) -> &str { match *self { Self::Authenticator => "Enter the 6 digit verification code from your authenticator app.", Self::Yubikey => "Insert your Yubikey and push the button.", Self::Email => "Enter the PIN you received via email.", _ => "Enter the code." } } #[must_use] pub fn header(&self) -> &str { match *self { Self::Authenticator => "Authenticator App", Self::Yubikey => "Yubikey", Self::Email => "Email Code", _ => "Two Factor Authentication", } } #[must_use] pub fn grab(&self) -> bool { !matches!(self, Self::Email) } } impl<'de> serde::Deserialize<'de> for TwoFactorProviderType { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { struct TwoFactorProviderTypeVisitor; impl<'de> serde::de::Visitor<'de> for TwoFactorProviderTypeVisitor { type Value = TwoFactorProviderType; fn expecting( &self, formatter: &mut std::fmt::Formatter, ) -> std::fmt::Result { formatter.write_str("two factor provider id") } fn visit_str( self, value: &str, ) -> std::result::Result where E: serde::de::Error, { value.parse().map_err(serde::de::Error::custom) } fn visit_u64( self, value: u64, ) -> std::result::Result where E: serde::de::Error, { std::convert::TryFrom::try_from(value) .map_err(serde::de::Error::custom) } } deserializer.deserialize_any(TwoFactorProviderTypeVisitor) } } impl std::convert::TryFrom for TwoFactorProviderType { type Error = Error; fn try_from(ty: u64) -> Result { match ty { 0 => Ok(Self::Authenticator), 1 => Ok(Self::Email), 2 => Ok(Self::Duo), 3 => Ok(Self::Yubikey), 4 => Ok(Self::U2f), 5 => Ok(Self::Remember), 6 => Ok(Self::OrganizationDuo), 7 => Ok(Self::WebAuthn), _ => Err(Error::InvalidTwoFactorProvider { ty: format!("{ty}"), }), } } } impl std::str::FromStr for TwoFactorProviderType { type Err = Error; fn from_str(ty: &str) -> Result { match ty { "0" => Ok(Self::Authenticator), "1" => Ok(Self::Email), "2" => Ok(Self::Duo), "3" => Ok(Self::Yubikey), "4" => Ok(Self::U2f), "5" => Ok(Self::Remember), "6" => Ok(Self::OrganizationDuo), "7" => Ok(Self::WebAuthn), _ => Err(Error::InvalidTwoFactorProvider { ty: ty.to_string() }), } } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum KdfType { Pbkdf2 = 0, Argon2id = 1, } impl<'de> serde::Deserialize<'de> for KdfType { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { struct KdfTypeVisitor; impl<'de> serde::de::Visitor<'de> for KdfTypeVisitor { type Value = KdfType; fn expecting( &self, formatter: &mut std::fmt::Formatter, ) -> std::fmt::Result { formatter.write_str("kdf id") } fn visit_str( self, value: &str, ) -> std::result::Result where E: serde::de::Error, { value.parse().map_err(serde::de::Error::custom) } fn visit_u64( self, value: u64, ) -> std::result::Result where E: serde::de::Error, { std::convert::TryFrom::try_from(value) .map_err(serde::de::Error::custom) } } deserializer.deserialize_any(KdfTypeVisitor) } } impl std::convert::TryFrom for KdfType { type Error = Error; fn try_from(ty: u64) -> Result { match ty { 0 => Ok(Self::Pbkdf2), 1 => Ok(Self::Argon2id), _ => Err(Error::InvalidKdfType { ty: format!("{ty}"), }), } } } impl std::str::FromStr for KdfType { type Err = Error; fn from_str(ty: &str) -> Result { match ty { "0" => Ok(Self::Pbkdf2), "1" => Ok(Self::Argon2id), _ => Err(Error::InvalidKdfType { ty: ty.to_string() }), } } } impl serde::Serialize for KdfType { fn serialize( &self, serializer: S, ) -> std::result::Result where S: serde::Serializer, { let s = match self { Self::Pbkdf2 => "0", Self::Argon2id => "1", }; serializer.serialize_str(s) } } #[derive(serde::Serialize, Debug)] struct PreloginReq { email: String, } #[derive(serde::Deserialize, Debug)] struct PreloginRes { #[serde(rename = "Kdf", alias = "kdf")] kdf: KdfType, #[serde(rename = "KdfIterations", alias = "kdfIterations")] kdf_iterations: u32, #[serde(rename = "KdfMemory", alias = "kdfMemory")] kdf_memory: Option, #[serde(rename = "KdfParallelism", alias = "kdfParallelism")] kdf_parallelism: Option, } #[derive(serde::Serialize, Debug)] struct ConnectPasswordReq { grant_type: String, username: String, password: Option, scope: String, client_id: String, client_secret: Option, #[serde(rename = "deviceType")] device_type: u32, #[serde(rename = "deviceIdentifier")] device_identifier: String, #[serde(rename = "deviceName")] device_name: String, #[serde(rename = "devicePushToken")] device_push_token: String, #[serde(rename = "twoFactorToken")] two_factor_token: Option, #[serde(rename = "twoFactorProvider")] two_factor_provider: Option, } #[derive(serde::Deserialize, Debug)] struct ConnectPasswordRes { access_token: String, refresh_token: String, #[serde(rename = "Key", alias = "key")] key: String, } #[derive(serde::Deserialize, Debug)] struct ConnectErrorRes { error: String, error_description: Option, #[serde(rename = "ErrorModel", alias = "errorModel")] error_model: Option, #[serde(rename = "TwoFactorProviders", alias = "twoFactorProviders")] two_factor_providers: Option>, } #[derive(serde::Deserialize, Debug)] struct ConnectErrorResErrorModel { #[serde(rename = "Message", alias = "message")] message: String, } #[derive(serde::Serialize, Debug)] struct ConnectRefreshTokenReq { grant_type: String, client_id: String, refresh_token: String, } #[derive(serde::Deserialize, Debug)] struct ConnectRefreshTokenRes { access_token: String, } #[derive(serde::Deserialize, Debug)] struct SyncRes { #[serde(rename = "Ciphers", alias = "ciphers")] ciphers: Vec, #[serde(rename = "Profile", alias = "profile")] profile: SyncResProfile, #[serde(rename = "Folders", alias = "folders")] folders: Vec, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResCipher { #[serde(rename = "Id", alias = "id")] id: String, #[serde(rename = "FolderId", alias = "folderId")] folder_id: Option, #[serde(rename = "OrganizationId", alias = "organizationId")] organization_id: Option, #[serde(rename = "Name", alias = "name")] name: String, #[serde(rename = "Login", alias = "login")] login: Option, #[serde(rename = "Card", alias = "card")] card: Option, #[serde(rename = "Identity", alias = "identity")] identity: Option, #[serde(rename = "SecureNote", alias = "secureNote")] secure_note: Option, #[serde(rename = "Notes", alias = "notes")] notes: Option, #[serde(rename = "PasswordHistory", alias = "passwordHistory")] password_history: Option>, #[serde(rename = "Fields", alias = "fields")] fields: Option>, #[serde(rename = "DeletedDate", alias = "deletedDate")] deleted_date: Option, } impl SyncResCipher { fn to_entry( &self, folders: &[SyncResFolder], ) -> Option { if self.deleted_date.is_some() { return None; } let history = self.password_history .as_ref() .map_or_else(Vec::new, |history| { history .iter() .filter_map(|entry| { // Gets rid of entries with a non-existent // password entry.password.clone().map(|p| { crate::db::HistoryEntry { last_used_date: entry .last_used_date .clone(), password: p, } }) }) .collect() }); let (folder, folder_id) = self.folder_id.as_ref().map_or((None, None), |folder_id| { let mut folder_name = None; for folder in folders { if &folder.id == folder_id { folder_name = Some(folder.name.clone()); } } (folder_name, Some(folder_id)) }); let data = if let Some(login) = &self.login { crate::db::EntryData::Login { username: login.username.clone(), password: login.password.clone(), totp: login.totp.clone(), uris: login.uris.as_ref().map_or_else( std::vec::Vec::new, |uris| { uris.iter() .filter_map(|uri| { uri.uri.clone().map(|s| crate::db::Uri { uri: s, match_type: uri.match_type, }) }) .collect() }, ), } } else if let Some(card) = &self.card { crate::db::EntryData::Card { cardholder_name: card.cardholder_name.clone(), number: card.number.clone(), brand: card.brand.clone(), exp_month: card.exp_month.clone(), exp_year: card.exp_year.clone(), code: card.code.clone(), } } else if let Some(identity) = &self.identity { crate::db::EntryData::Identity { title: identity.title.clone(), first_name: identity.first_name.clone(), middle_name: identity.middle_name.clone(), last_name: identity.last_name.clone(), address1: identity.address1.clone(), address2: identity.address2.clone(), address3: identity.address3.clone(), city: identity.city.clone(), state: identity.state.clone(), postal_code: identity.postal_code.clone(), country: identity.country.clone(), phone: identity.phone.clone(), email: identity.email.clone(), ssn: identity.ssn.clone(), license_number: identity.license_number.clone(), passport_number: identity.passport_number.clone(), username: identity.username.clone(), } } else if let Some(_secure_note) = &self.secure_note { crate::db::EntryData::SecureNote } else { return None; }; let fields = self.fields.as_ref().map_or_else(Vec::new, |fields| { fields .iter() .map(|field| crate::db::Field { ty: field.ty, name: field.name.clone(), value: field.value.clone(), linked_id: field.linked_id, }) .collect() }); Some(crate::db::Entry { id: self.id.clone(), org_id: self.organization_id.clone(), folder, folder_id: folder_id.map(std::string::ToString::to_string), name: self.name.clone(), data, fields, notes: self.notes.clone(), history, }) } } #[derive(serde::Deserialize, Debug)] struct SyncResProfile { #[serde(rename = "Key", alias = "key")] key: String, #[serde(rename = "PrivateKey", alias = "privateKey")] private_key: String, #[serde(rename = "Organizations", alias = "organizations")] organizations: Vec, } #[derive(serde::Deserialize, Debug)] struct SyncResProfileOrganization { #[serde(rename = "Id", alias = "id")] id: String, #[serde(rename = "Key", alias = "key")] key: String, } #[derive(serde::Deserialize, Debug, Clone)] struct SyncResFolder { #[serde(rename = "Id", alias = "id")] id: String, #[serde(rename = "Name", alias = "name")] name: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherLogin { #[serde(rename = "Username", alias = "username")] username: Option, #[serde(rename = "Password", alias = "password")] password: Option, #[serde(rename = "Totp", alias = "totp")] totp: Option, #[serde(rename = "Uris", alias = "uris")] uris: Option>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherLoginUri { #[serde(rename = "Uri", alias = "uri")] uri: Option, #[serde(rename = "Match", alias = "match")] match_type: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherCard { #[serde(rename = "CardholderName", alias = "cardHolderName")] cardholder_name: Option, #[serde(rename = "Number", alias = "number")] number: Option, #[serde(rename = "Brand", alias = "brand")] brand: Option, #[serde(rename = "ExpMonth", alias = "expMonth")] exp_month: Option, #[serde(rename = "ExpYear", alias = "expYear")] exp_year: Option, #[serde(rename = "Code", alias = "code")] code: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherIdentity { #[serde(rename = "Title", alias = "title")] title: Option, #[serde(rename = "FirstName", alias = "firstName")] first_name: Option, #[serde(rename = "MiddleName", alias = "middleName")] middle_name: Option, #[serde(rename = "LastName", alias = "lastName")] last_name: Option, #[serde(rename = "Address1", alias = "address1")] address1: Option, #[serde(rename = "Address2", alias = "address2")] address2: Option, #[serde(rename = "Address3", alias = "address3")] address3: Option, #[serde(rename = "City", alias = "city")] city: Option, #[serde(rename = "State", alias = "state")] state: Option, #[serde(rename = "PostalCode", alias = "postalCode")] postal_code: Option, #[serde(rename = "Country", alias = "country")] country: Option, #[serde(rename = "Phone", alias = "phone")] phone: Option, #[serde(rename = "Email", alias = "email")] email: Option, #[serde(rename = "SSN", alias = "ssn")] ssn: Option, #[serde(rename = "LicenseNumber", alias = "licenseNumber")] license_number: Option, #[serde(rename = "PassportNumber", alias = "passportNumber")] passport_number: Option, #[serde(rename = "Username", alias = "username")] username: Option, } #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Clone, Copy, PartialEq, Eq, )] #[repr(u16)] pub enum FieldType { Text = 0, Hidden = 1, Boolean = 2, Linked = 3, } #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Clone, Copy, PartialEq, Eq, )] #[repr(u16)] pub enum LinkedIdType { LoginUsername = 100, LoginPassword = 101, CardCardholderName = 300, CardExpMonth = 301, CardExpYear = 302, CardCode = 303, CardBrand = 304, CardNumber = 305, IdentityTitle = 400, IdentityMiddleName = 401, IdentityAddress1 = 402, IdentityAddress2 = 403, IdentityAddress3 = 404, IdentityCity = 405, IdentityState = 406, IdentityPostalCode = 407, IdentityCountry = 408, IdentityCompany = 409, IdentityEmail = 410, IdentityPhone = 411, IdentitySsn = 412, IdentityUsername = 413, IdentityPassportNumber = 414, IdentityLicenseNumber = 415, IdentityFirstName = 416, IdentityLastName = 417, IdentityFullName = 418, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherField { #[serde(rename = "Type", alias = "type")] ty: FieldType, #[serde(rename = "Name", alias = "name")] name: Option, #[serde(rename = "Value", alias = "value")] value: Option, #[serde(rename = "LinkedId", alias = "linkedId")] linked_id: Option, } // this is just a name and some notes, both of which are already on the cipher // object #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherSecureNote {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResPasswordHistory { #[serde(rename = "LastUsedDate", alias = "lastUsedDate")] last_used_date: String, #[serde(rename = "Password", alias = "password")] password: Option, } #[derive(serde::Serialize, Debug)] struct CiphersPostReq { #[serde(rename = "type")] ty: u32, // XXX what are the valid types? #[serde(rename = "folderId")] folder_id: Option, name: String, notes: Option, login: Option, card: Option, identity: Option, #[serde(rename = "secureNote")] secure_note: Option, } #[derive(serde::Serialize, Debug)] struct CiphersPutReq { #[serde(rename = "type")] ty: u32, // XXX what are the valid types? #[serde(rename = "folderId")] folder_id: Option, #[serde(rename = "organizationId")] organization_id: Option, name: String, notes: Option, login: Option, card: Option, identity: Option, fields: Vec, #[serde(rename = "secureNote")] secure_note: Option, #[serde(rename = "passwordHistory")] password_history: Vec, } #[derive(serde::Serialize, Debug)] struct CiphersPutReqLogin { username: Option, password: Option, } #[derive(serde::Serialize, Debug)] struct CiphersPutReqHistory { #[serde(rename = "LastUsedDate")] last_used_date: String, #[serde(rename = "Password")] password: String, } #[derive(serde::Deserialize, Debug)] struct FoldersRes { #[serde(rename = "Data", alias = "data")] data: Vec, } #[derive(serde::Deserialize, Debug)] struct FoldersResData { #[serde(rename = "Id", alias = "id")] id: String, #[serde(rename = "Name", alias = "name")] name: String, } #[derive(serde::Serialize, Debug)] struct FoldersPostReq { name: String, } #[derive(Debug)] pub struct Client { base_url: String, identity_url: String, client_cert_path: Option, } impl Client { #[must_use] pub fn new( base_url: &str, identity_url: &str, client_cert_path: Option<&std::path::Path>, ) -> Self { Self { base_url: base_url.to_string(), identity_url: identity_url.to_string(), client_cert_path: client_cert_path .map(std::path::Path::to_path_buf), } } async fn reqwest_client(&self) -> Result { if let Some(client_cert_path) = self.client_cert_path.as_ref() { let mut buf = Vec::new(); let mut f = tokio::fs::File::open(client_cert_path) .await .map_err(|e| Error::LoadClientCert { source: e, file: client_cert_path.clone(), })?; f.read_to_end(&mut buf).await.map_err(|e| { Error::LoadClientCert { source: e, file: client_cert_path.clone(), } })?; let pem = reqwest::Identity::from_pem(&buf) .map_err(|e| Error::CreateReqwestClient { source: e })?; Ok(reqwest::Client::builder() .user_agent(format!( "{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") )) .identity(pem) .build() .map_err(|e| Error::CreateReqwestClient { source: e })?) } else { Ok(reqwest::Client::builder() .user_agent(format!( "{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") )) .build() .map_err(|e| Error::CreateReqwestClient { source: e })?) } } pub async fn prelogin( &self, email: &str, ) -> Result<(KdfType, u32, Option, Option)> { let prelogin = PreloginReq { email: email.to_string(), }; let client = self.reqwest_client().await?; let res = client .post(&self.api_url("/accounts/prelogin")) .json(&prelogin) .send() .await .map_err(|source| Error::Reqwest { source })?; let prelogin_res: PreloginRes = res.json_with_path().await?; Ok(( prelogin_res.kdf, prelogin_res.kdf_iterations, prelogin_res.kdf_memory, prelogin_res.kdf_parallelism, )) } pub async fn register( &self, email: &str, device_id: &str, apikey: &crate::locked::ApiKey, ) -> Result<()> { let connect_req = ConnectPasswordReq { grant_type: "client_credentials".to_string(), username: email.to_string(), password: None, scope: "api".to_string(), // XXX unwraps here are not necessarily safe client_id: String::from_utf8(apikey.client_id().to_vec()) .unwrap(), client_secret: Some( String::from_utf8(apikey.client_secret().to_vec()).unwrap(), ), device_type: 8, device_identifier: device_id.to_string(), device_name: "rbw".to_string(), device_push_token: String::new(), two_factor_token: None, two_factor_provider: None, }; let client = self.reqwest_client().await?; let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) .send() .await .map_err(|source| Error::Reqwest { source })?; if res.status() == reqwest::StatusCode::OK { Ok(()) } else { let code = res.status().as_u16(); match res.text().await { Ok(body) => match body.clone().json_with_path() { Ok(json) => Err(classify_login_error(&json, code)), Err(e) => { log::warn!("{e}: {body}"); Err(Error::RequestFailed { status: code }) } }, Err(e) => { log::warn!("failed to read response body: {e}"); Err(Error::RequestFailed { status: code }) } } } } pub async fn login( &self, email: &str, device_id: &str, password_hash: &crate::locked::PasswordHash, two_factor_token: Option<&str>, two_factor_provider: Option, ) -> Result<(String, String, String)> { let connect_req = ConnectPasswordReq { grant_type: "password".to_string(), username: email.to_string(), password: Some(crate::base64::encode(password_hash.hash())), scope: "api offline_access".to_string(), client_id: "desktop".to_string(), client_secret: None, device_type: 8, device_identifier: device_id.to_string(), device_name: "rbw".to_string(), device_push_token: String::new(), two_factor_token: two_factor_token .map(std::string::ToString::to_string), two_factor_provider: two_factor_provider.map(|ty| ty as u32), }; let client = self.reqwest_client().await?; let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) .header( "auth-email", crate::base64::encode_url_safe_no_pad(email), ) .send() .await .map_err(|source| Error::Reqwest { source })?; if res.status() == reqwest::StatusCode::OK { let connect_res: ConnectPasswordRes = res.json_with_path().await?; Ok(( connect_res.access_token, connect_res.refresh_token, connect_res.key, )) } else { let code = res.status().as_u16(); match res.text().await { Ok(body) => match body.clone().json_with_path() { Ok(json) => Err(classify_login_error(&json, code)), Err(e) => { log::warn!("{e}: {body}"); Err(Error::RequestFailed { status: code }) } }, Err(e) => { log::warn!("failed to read response body: {e}"); Err(Error::RequestFailed { status: code }) } } } } pub async fn sync( &self, access_token: &str, ) -> Result<( String, String, std::collections::HashMap, Vec, )> { let client = self.reqwest_client().await?; let res = client .get(&self.api_url("/sync")) .header("Authorization", format!("Bearer {access_token}")) .send() .await .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => { let sync_res: SyncRes = res.json_with_path().await?; let folders = sync_res.folders.clone(); let ciphers = sync_res .ciphers .iter() .filter_map(|cipher| cipher.to_entry(&folders)) .collect(); let org_keys = sync_res .profile .organizations .iter() .map(|org| (org.id.clone(), org.key.clone())) .collect(); Ok(( sync_res.profile.key, sync_res.profile.private_key, org_keys, ciphers, )) } reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn add( &self, access_token: &str, name: &str, data: &crate::db::EntryData, notes: Option<&str>, folder_id: Option<&str>, ) -> Result<()> { let mut req = CiphersPostReq { ty: 1, folder_id: folder_id.map(std::string::ToString::to_string), name: name.to_string(), notes: notes.map(std::string::ToString::to_string), login: None, card: None, identity: None, secure_note: None, }; match data { crate::db::EntryData::Login { username, password, totp, uris, } => { let uris = if uris.is_empty() { None } else { Some( uris.iter() .map(|s| CipherLoginUri { uri: Some(s.uri.to_string()), match_type: s.match_type, }) .collect(), ) }; req.login = Some(CipherLogin { username: username.clone(), password: password.clone(), totp: totp.clone(), uris, }); } crate::db::EntryData::Card { cardholder_name, number, brand, exp_month, exp_year, code, } => { req.card = Some(CipherCard { cardholder_name: cardholder_name.clone(), number: number.clone(), brand: brand.clone(), exp_month: exp_month.clone(), exp_year: exp_year.clone(), code: code.clone(), }); } crate::db::EntryData::Identity { title, first_name, middle_name, last_name, address1, address2, address3, city, state, postal_code, country, phone, email, ssn, license_number, passport_number, username, } => { req.identity = Some(CipherIdentity { title: title.clone(), first_name: first_name.clone(), middle_name: middle_name.clone(), last_name: last_name.clone(), address1: address1.clone(), address2: address2.clone(), address3: address3.clone(), city: city.clone(), state: state.clone(), postal_code: postal_code.clone(), country: country.clone(), phone: phone.clone(), email: email.clone(), ssn: ssn.clone(), license_number: license_number.clone(), passport_number: passport_number.clone(), username: username.clone(), }); } crate::db::EntryData::SecureNote {} => { req.secure_note = Some(CipherSecureNote {}); } } let client = reqwest::blocking::Client::new(); let res = client .post(self.api_url("/ciphers")) .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => Ok(()), reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn edit( &self, access_token: &str, id: &str, org_id: Option<&str>, name: &str, data: &crate::db::EntryData, fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<()> { let mut req = CiphersPutReq { ty: match data { crate::db::EntryData::Login { .. } => 1, crate::db::EntryData::SecureNote { .. } => 2, crate::db::EntryData::Card { .. } => 3, crate::db::EntryData::Identity { .. } => 4, }, folder_id: folder_uuid.map(std::string::ToString::to_string), organization_id: org_id.map(std::string::ToString::to_string), name: name.to_string(), notes: notes.map(std::string::ToString::to_string), login: None, card: None, identity: None, secure_note: None, fields: fields .iter() .map(|field| CipherField { ty: field.ty, name: field.name.clone(), value: field.value.clone(), linked_id: field.linked_id, }) .collect(), password_history: history .iter() .map(|entry| CiphersPutReqHistory { last_used_date: entry.last_used_date.clone(), password: entry.password.clone(), }) .collect(), }; match data { crate::db::EntryData::Login { username, password, totp, uris, } => { let uris = if uris.is_empty() { None } else { Some( uris.iter() .map(|s| CipherLoginUri { uri: Some(s.uri.to_string()), match_type: s.match_type, }) .collect(), ) }; req.login = Some(CipherLogin { username: username.clone(), password: password.clone(), totp: totp.clone(), uris, }); } crate::db::EntryData::Card { cardholder_name, number, brand, exp_month, exp_year, code, } => { req.card = Some(CipherCard { cardholder_name: cardholder_name.clone(), number: number.clone(), brand: brand.clone(), exp_month: exp_month.clone(), exp_year: exp_year.clone(), code: code.clone(), }); } crate::db::EntryData::Identity { title, first_name, middle_name, last_name, address1, address2, address3, city, state, postal_code, country, phone, email, ssn, license_number, passport_number, username, } => { req.identity = Some(CipherIdentity { title: title.clone(), first_name: first_name.clone(), middle_name: middle_name.clone(), last_name: last_name.clone(), address1: address1.clone(), address2: address2.clone(), address3: address3.clone(), city: city.clone(), state: state.clone(), postal_code: postal_code.clone(), country: country.clone(), phone: phone.clone(), email: email.clone(), ssn: ssn.clone(), license_number: license_number.clone(), passport_number: passport_number.clone(), username: username.clone(), }); } crate::db::EntryData::SecureNote {} => { req.secure_note = Some(CipherSecureNote {}); } } let client = reqwest::blocking::Client::new(); let res = client .put(self.api_url(&format!("/ciphers/{id}"))) .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => Ok(()), reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn remove(&self, access_token: &str, id: &str) -> Result<()> { let client = reqwest::blocking::Client::new(); let res = client .delete(self.api_url(&format!("/ciphers/{id}"))) .header("Authorization", format!("Bearer {access_token}")) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => Ok(()), reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn folders( &self, access_token: &str, ) -> Result> { let client = reqwest::blocking::Client::new(); let res = client .get(self.api_url("/folders")) .header("Authorization", format!("Bearer {access_token}")) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => { let folders_res: FoldersRes = res.json_with_path()?; Ok(folders_res .data .iter() .map(|folder| (folder.id.clone(), folder.name.clone())) .collect()) } reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn create_folder( &self, access_token: &str, name: &str, ) -> Result { let req = FoldersPostReq { name: name.to_string(), }; let client = reqwest::blocking::Client::new(); let res = client .post(self.api_url("/folders")) .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { reqwest::StatusCode::OK => { let folders_res: FoldersResData = res.json_with_path()?; Ok(folders_res.id) } reqwest::StatusCode::UNAUTHORIZED => { Err(Error::RequestUnauthorized) } _ => Err(Error::RequestFailed { status: res.status().as_u16(), }), } } pub fn exchange_refresh_token( &self, refresh_token: &str, ) -> Result { let connect_req = ConnectRefreshTokenReq { grant_type: "refresh_token".to_string(), client_id: "desktop".to_string(), refresh_token: refresh_token.to_string(), }; let client = reqwest::blocking::Client::new(); let res = client .post(self.identity_url("/connect/token")) .form(&connect_req) .send() .map_err(|source| Error::Reqwest { source })?; let connect_res: ConnectRefreshTokenRes = res.json_with_path()?; Ok(connect_res.access_token) } pub async fn exchange_refresh_token_async( &self, refresh_token: &str, ) -> Result { let connect_req = ConnectRefreshTokenReq { grant_type: "refresh_token".to_string(), client_id: "desktop".to_string(), refresh_token: refresh_token.to_string(), }; let client = self.reqwest_client().await?; let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) .send() .await .map_err(|source| Error::Reqwest { source })?; let connect_res: ConnectRefreshTokenRes = res.json_with_path().await?; Ok(connect_res.access_token) } fn api_url(&self, path: &str) -> String { format!("{}{}", self.base_url, path) } fn identity_url(&self, path: &str) -> String { format!("{}{}", self.identity_url, path) } } fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error { let error_desc = error_res.error_description.clone(); let error_desc = error_desc.as_deref(); match error_res.error.as_str() { "invalid_grant" => match error_desc { Some("invalid_username_or_password") => { if let Some(error_model) = error_res.error_model.as_ref() { let message = error_model.message.as_str().to_string(); return Error::IncorrectPassword { message }; } } Some("Two factor required.") => { if let Some(providers) = error_res.two_factor_providers.as_ref() { return Error::TwoFactorRequired { providers: providers.clone(), }; } } Some("Captcha required.") => { return Error::RegistrationRequired; } _ => {} }, "invalid_client" => { return Error::IncorrectApiKey; } "" => { // bitwarden_rs returns an empty error and error_description for // this case, for some reason if error_desc.is_none() || error_desc == Some("") { if let Some(error_model) = error_res.error_model.as_ref() { let message = error_model.message.as_str().to_string(); match message.as_str() { "Username or password is incorrect. Try again" | "TOTP code is not a number" => { return Error::IncorrectPassword { message }; } s => { if s.starts_with( "Invalid TOTP code! Server time: ", ) { return Error::IncorrectPassword { message }; } } } } } } _ => {} } log::warn!("unexpected error received during login: {:?}", error_res); Error::RequestFailed { status: code } }