diff options
Diffstat (limited to 'src/api.rs')
-rw-r--r-- | src/api.rs | 556 |
1 files changed, 418 insertions, 138 deletions
@@ -1,9 +1,15 @@ +// serde_repr generates some as conversions that we can't seem to silence from +// here, unfortunately +#![allow(clippy::as_conversions)] + use crate::prelude::*; use crate::json::{ DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _, }; +use tokio::io::AsyncReadExt as _; + #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, @@ -35,7 +41,7 @@ impl std::fmt::Display for UriMatchType { RegularExpression => "regular_expression", Never => "never", }; - write!(f, "{}", s) + write!(f, "{s}") } } @@ -51,6 +57,33 @@ pub enum TwoFactorProviderType { WebAuthn = 7, } +impl TwoFactorProviderType { + #[must_use] + pub fn message(&self) -> &str { + match *self { + Self::Authenticator => "Enter the 6 digit verification code from your authenticator app.", + Self::Yubikey => "Insert your Yubikey and push the button.", + Self::Email => "Enter the PIN you received via email.", + _ => "Enter the code." + } + } + + #[must_use] + pub fn header(&self) -> &str { + match *self { + Self::Authenticator => "Authenticator App", + Self::Yubikey => "Yubikey", + Self::Email => "Email Code", + _ => "Two Factor Authentication", + } + } + + #[must_use] + pub fn grab(&self) -> bool { + !matches!(self, Self::Email) + } +} + impl<'de> serde::Deserialize<'de> for TwoFactorProviderType { fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> where @@ -107,7 +140,7 @@ impl std::convert::TryFrom<u64> for TwoFactorProviderType { 6 => Ok(Self::OrganizationDuo), 7 => Ok(Self::WebAuthn), _ => Err(Error::InvalidTwoFactorProvider { - ty: format!("{}", ty), + ty: format!("{ty}"), }), } } @@ -131,6 +164,96 @@ impl std::str::FromStr for TwoFactorProviderType { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum KdfType { + Pbkdf2 = 0, + Argon2id = 1, +} + +impl<'de> serde::Deserialize<'de> for KdfType { + fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct KdfTypeVisitor; + impl<'de> serde::de::Visitor<'de> for KdfTypeVisitor { + type Value = KdfType; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + formatter.write_str("kdf id") + } + + fn visit_str<E>( + self, + value: &str, + ) -> std::result::Result<Self::Value, E> + where + E: serde::de::Error, + { + value.parse().map_err(serde::de::Error::custom) + } + + fn visit_u64<E>( + self, + value: u64, + ) -> std::result::Result<Self::Value, E> + where + E: serde::de::Error, + { + std::convert::TryFrom::try_from(value) + .map_err(serde::de::Error::custom) + } + } + + deserializer.deserialize_any(KdfTypeVisitor) + } +} + +impl std::convert::TryFrom<u64> for KdfType { + type Error = Error; + + fn try_from(ty: u64) -> Result<Self> { + match ty { + 0 => Ok(Self::Pbkdf2), + 1 => Ok(Self::Argon2id), + _ => Err(Error::InvalidKdfType { + ty: format!("{ty}"), + }), + } + } +} + +impl std::str::FromStr for KdfType { + type Err = Error; + + fn from_str(ty: &str) -> Result<Self> { + match ty { + "0" => Ok(Self::Pbkdf2), + "1" => Ok(Self::Argon2id), + _ => Err(Error::InvalidKdfType { ty: ty.to_string() }), + } + } +} + +impl serde::Serialize for KdfType { + fn serialize<S>( + &self, + serializer: S, + ) -> std::result::Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let s = match self { + Self::Pbkdf2 => "0", + Self::Argon2id => "1", + }; + serializer.serialize_str(s) + } +} + #[derive(serde::Serialize, Debug)] struct PreloginReq { email: String, @@ -138,10 +261,14 @@ struct PreloginReq { #[derive(serde::Deserialize, Debug)] struct PreloginRes { - #[serde(rename = "Kdf")] - kdf: u32, - #[serde(rename = "KdfIterations")] + #[serde(rename = "Kdf", alias = "kdf")] + kdf: KdfType, + #[serde(rename = "KdfIterations", alias = "kdfIterations")] kdf_iterations: u32, + #[serde(rename = "KdfMemory", alias = "kdfMemory")] + kdf_memory: Option<u32>, + #[serde(rename = "KdfParallelism", alias = "kdfParallelism")] + kdf_parallelism: Option<u32>, } #[derive(serde::Serialize, Debug)] @@ -169,10 +296,8 @@ struct ConnectPasswordReq { #[derive(serde::Deserialize, Debug)] struct ConnectPasswordRes { access_token: String, - expires_in: u32, - token_type: String, refresh_token: String, - #[serde(rename = "Key")] + #[serde(rename = "Key", alias = "key")] key: String, } @@ -180,15 +305,15 @@ struct ConnectPasswordRes { struct ConnectErrorRes { error: String, error_description: Option<String>, - #[serde(rename = "ErrorModel")] + #[serde(rename = "ErrorModel", alias = "errorModel")] error_model: Option<ConnectErrorResErrorModel>, - #[serde(rename = "TwoFactorProviders")] + #[serde(rename = "TwoFactorProviders", alias = "twoFactorProviders")] two_factor_providers: Option<Vec<TwoFactorProviderType>>, } #[derive(serde::Deserialize, Debug)] struct ConnectErrorResErrorModel { - #[serde(rename = "Message")] + #[serde(rename = "Message", alias = "message")] message: String, } @@ -202,46 +327,43 @@ struct ConnectRefreshTokenReq { #[derive(serde::Deserialize, Debug)] struct ConnectRefreshTokenRes { access_token: String, - expires_in: u32, - token_type: String, - refresh_token: String, } #[derive(serde::Deserialize, Debug)] struct SyncRes { - #[serde(rename = "Ciphers")] + #[serde(rename = "Ciphers", alias = "ciphers")] ciphers: Vec<SyncResCipher>, - #[serde(rename = "Profile")] + #[serde(rename = "Profile", alias = "profile")] profile: SyncResProfile, - #[serde(rename = "Folders")] + #[serde(rename = "Folders", alias = "folders")] folders: Vec<SyncResFolder>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResCipher { - #[serde(rename = "Id")] + #[serde(rename = "Id", alias = "id")] id: String, - #[serde(rename = "FolderId")] + #[serde(rename = "FolderId", alias = "folderId")] folder_id: Option<String>, - #[serde(rename = "OrganizationId")] + #[serde(rename = "OrganizationId", alias = "organizationId")] organization_id: Option<String>, - #[serde(rename = "Name")] + #[serde(rename = "Name", alias = "name")] name: String, - #[serde(rename = "Login")] + #[serde(rename = "Login", alias = "login")] login: Option<CipherLogin>, - #[serde(rename = "Card")] + #[serde(rename = "Card", alias = "card")] card: Option<CipherCard>, - #[serde(rename = "Identity")] + #[serde(rename = "Identity", alias = "identity")] identity: Option<CipherIdentity>, - #[serde(rename = "SecureNote")] + #[serde(rename = "SecureNote", alias = "secureNote")] secure_note: Option<CipherSecureNote>, - #[serde(rename = "Notes")] + #[serde(rename = "Notes", alias = "notes")] notes: Option<String>, - #[serde(rename = "PasswordHistory")] + #[serde(rename = "PasswordHistory", alias = "passwordHistory")] password_history: Option<Vec<SyncResPasswordHistory>>, - #[serde(rename = "Fields")] - fields: Option<Vec<SyncResField>>, - #[serde(rename = "DeletedDate")] + #[serde(rename = "Fields", alias = "fields")] + fields: Option<Vec<CipherField>>, + #[serde(rename = "DeletedDate", alias = "deletedDate")] deleted_date: Option<String>, } @@ -253,32 +375,37 @@ impl SyncResCipher { if self.deleted_date.is_some() { return None; } - let history = if let Some(history) = &self.password_history { - history - .iter() - .filter_map(|entry| { - // Gets rid of entries with a non-existent password - entry.password.clone().map(|p| crate::db::HistoryEntry { - last_used_date: entry.last_used_date.clone(), - password: p, - }) - }) - .collect() - } else { - vec![] - }; + let history = + self.password_history + .as_ref() + .map_or_else(Vec::new, |history| { + history + .iter() + .filter_map(|entry| { + // Gets rid of entries with a non-existent + // password + entry.password.clone().map(|p| { + crate::db::HistoryEntry { + last_used_date: entry + .last_used_date + .clone(), + password: p, + } + }) + }) + .collect() + }); - let (folder, folder_id) = if let Some(folder_id) = &self.folder_id { - let mut folder_name = None; - for folder in folders { - if &folder.id == folder_id { - folder_name = Some(folder.name.clone()); + let (folder, folder_id) = + self.folder_id.as_ref().map_or((None, None), |folder_id| { + let mut folder_name = None; + for folder in folders { + if &folder.id == folder_id { + folder_name = Some(folder.name.clone()); + } } - } - (folder_name, Some(folder_id)) - } else { - (None, None) - }; + (folder_name, Some(folder_id)) + }); let data = if let Some(login) = &self.login { crate::db::EntryData::Login { username: login.username.clone(), @@ -332,17 +459,17 @@ impl SyncResCipher { } else { return None; }; - let fields = if let Some(fields) = &self.fields { + let fields = self.fields.as_ref().map_or_else(Vec::new, |fields| { fields .iter() .map(|field| crate::db::Field { + ty: field.ty, name: field.name.clone(), value: field.value.clone(), + linked_id: field.linked_id, }) .collect() - } else { - vec![] - }; + }); Some(crate::db::Entry { id: self.id.clone(), org_id: self.organization_id.clone(), @@ -359,104 +486,173 @@ impl SyncResCipher { #[derive(serde::Deserialize, Debug)] struct SyncResProfile { - #[serde(rename = "Key")] + #[serde(rename = "Key", alias = "key")] key: String, - #[serde(rename = "PrivateKey")] + #[serde(rename = "PrivateKey", alias = "privateKey")] private_key: String, - #[serde(rename = "Organizations")] + #[serde(rename = "Organizations", alias = "organizations")] organizations: Vec<SyncResProfileOrganization>, } #[derive(serde::Deserialize, Debug)] struct SyncResProfileOrganization { - #[serde(rename = "Id")] + #[serde(rename = "Id", alias = "id")] id: String, - #[serde(rename = "Key")] + #[serde(rename = "Key", alias = "key")] key: String, } #[derive(serde::Deserialize, Debug, Clone)] struct SyncResFolder { - #[serde(rename = "Id")] + #[serde(rename = "Id", alias = "id")] id: String, - #[serde(rename = "Name")] + #[serde(rename = "Name", alias = "name")] name: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherLogin { - #[serde(rename = "Username")] + #[serde(rename = "Username", alias = "username")] username: Option<String>, - #[serde(rename = "Password")] + #[serde(rename = "Password", alias = "password")] password: Option<String>, - #[serde(rename = "Totp")] + #[serde(rename = "Totp", alias = "totp")] totp: Option<String>, - #[serde(rename = "Uris")] + #[serde(rename = "Uris", alias = "uris")] uris: Option<Vec<CipherLoginUri>>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherLoginUri { - #[serde(rename = "Uri")] + #[serde(rename = "Uri", alias = "uri")] uri: Option<String>, - #[serde(rename = "Match")] + #[serde(rename = "Match", alias = "match")] match_type: Option<UriMatchType>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherCard { - #[serde(rename = "CardholderName")] + #[serde(rename = "CardholderName", alias = "cardHolderName")] cardholder_name: Option<String>, - #[serde(rename = "Number")] + #[serde(rename = "Number", alias = "number")] number: Option<String>, - #[serde(rename = "Brand")] + #[serde(rename = "Brand", alias = "brand")] brand: Option<String>, - #[serde(rename = "ExpMonth")] + #[serde(rename = "ExpMonth", alias = "expMonth")] exp_month: Option<String>, - #[serde(rename = "ExpYear")] + #[serde(rename = "ExpYear", alias = "expYear")] exp_year: Option<String>, - #[serde(rename = "Code")] + #[serde(rename = "Code", alias = "code")] code: Option<String>, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct CipherIdentity { - #[serde(rename = "Title")] + #[serde(rename = "Title", alias = "title")] title: Option<String>, - #[serde(rename = "FirstName")] + #[serde(rename = "FirstName", alias = "firstName")] first_name: Option<String>, - #[serde(rename = "MiddleName")] + #[serde(rename = "MiddleName", alias = "middleName")] middle_name: Option<String>, - #[serde(rename = "LastName")] + #[serde(rename = "LastName", alias = "lastName")] last_name: Option<String>, - #[serde(rename = "Address1")] + #[serde(rename = "Address1", alias = "address1")] address1: Option<String>, - #[serde(rename = "Address2")] + #[serde(rename = "Address2", alias = "address2")] address2: Option<String>, - #[serde(rename = "Address3")] + #[serde(rename = "Address3", alias = "address3")] address3: Option<String>, - #[serde(rename = "City")] + #[serde(rename = "City", alias = "city")] city: Option<String>, - #[serde(rename = "State")] + #[serde(rename = "State", alias = "state")] state: Option<String>, - #[serde(rename = "PostalCode")] + #[serde(rename = "PostalCode", alias = "postalCode")] postal_code: Option<String>, - #[serde(rename = "Country")] + #[serde(rename = "Country", alias = "country")] country: Option<String>, - #[serde(rename = "Phone")] + #[serde(rename = "Phone", alias = "phone")] phone: Option<String>, - #[serde(rename = "Email")] + #[serde(rename = "Email", alias = "email")] email: Option<String>, - #[serde(rename = "SSN")] + #[serde(rename = "SSN", alias = "ssn")] ssn: Option<String>, - #[serde(rename = "LicenseNumber")] + #[serde(rename = "LicenseNumber", alias = "licenseNumber")] license_number: Option<String>, - #[serde(rename = "PassportNumber")] + #[serde(rename = "PassportNumber", alias = "passportNumber")] passport_number: Option<String>, - #[serde(rename = "Username")] + #[serde(rename = "Username", alias = "username")] username: Option<String>, } +#[derive( + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, + Debug, + Clone, + Copy, + PartialEq, + Eq, +)] +#[repr(u16)] +pub enum FieldType { + Text = 0, + Hidden = 1, + Boolean = 2, + Linked = 3, +} + +#[derive( + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, + Debug, + Clone, + Copy, + PartialEq, + Eq, +)] +#[repr(u16)] +pub enum LinkedIdType { + LoginUsername = 100, + LoginPassword = 101, + CardCardholderName = 300, + CardExpMonth = 301, + CardExpYear = 302, + CardCode = 303, + CardBrand = 304, + CardNumber = 305, + IdentityTitle = 400, + IdentityMiddleName = 401, + IdentityAddress1 = 402, + IdentityAddress2 = 403, + IdentityAddress3 = 404, + IdentityCity = 405, + IdentityState = 406, + IdentityPostalCode = 407, + IdentityCountry = 408, + IdentityCompany = 409, + IdentityEmail = 410, + IdentityPhone = 411, + IdentitySsn = 412, + IdentityUsername = 413, + IdentityPassportNumber = 414, + IdentityLicenseNumber = 415, + IdentityFirstName = 416, + IdentityLastName = 417, + IdentityFullName = 418, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +struct CipherField { + #[serde(rename = "Type", alias = "type")] + ty: FieldType, + #[serde(rename = "Name", alias = "name")] + name: Option<String>, + #[serde(rename = "Value", alias = "value")] + value: Option<String>, + #[serde(rename = "LinkedId", alias = "linkedId")] + linked_id: Option<LinkedIdType>, +} + // this is just a name and some notes, both of which are already on the cipher // object #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] @@ -464,22 +660,12 @@ struct CipherSecureNote {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct SyncResPasswordHistory { - #[serde(rename = "LastUsedDate")] + #[serde(rename = "LastUsedDate", alias = "lastUsedDate")] last_used_date: String, - #[serde(rename = "Password")] + #[serde(rename = "Password", alias = "password")] password: Option<String>, } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -struct SyncResField { - #[serde(rename = "Type")] - ty: u32, - #[serde(rename = "Name")] - name: Option<String>, - #[serde(rename = "Value")] - value: Option<String>, -} - #[derive(serde::Serialize, Debug)] struct CiphersPostReq { #[serde(rename = "type")] @@ -508,6 +694,7 @@ struct CiphersPutReq { login: Option<CipherLogin>, card: Option<CipherCard>, identity: Option<CipherIdentity>, + fields: Vec<CipherField>, #[serde(rename = "secureNote")] secure_note: Option<CipherSecureNote>, #[serde(rename = "passwordHistory")] @@ -530,15 +717,15 @@ struct CiphersPutReqHistory { #[derive(serde::Deserialize, Debug)] struct FoldersRes { - #[serde(rename = "Data")] + #[serde(rename = "Data", alias = "data")] data: Vec<FoldersResData>, } #[derive(serde::Deserialize, Debug)] struct FoldersResData { - #[serde(rename = "Id")] + #[serde(rename = "Id", alias = "id")] id: String, - #[serde(rename = "Name")] + #[serde(rename = "Name", alias = "name")] name: String, } @@ -551,21 +738,70 @@ struct FoldersPostReq { pub struct Client { base_url: String, identity_url: String, + client_cert_path: Option<std::path::PathBuf>, } impl Client { - pub fn new(base_url: &str, identity_url: &str) -> Self { + #[must_use] + pub fn new( + base_url: &str, + identity_url: &str, + client_cert_path: Option<&std::path::Path>, + ) -> Self { Self { base_url: base_url.to_string(), identity_url: identity_url.to_string(), + client_cert_path: client_cert_path + .map(std::path::Path::to_path_buf), + } + } + + async fn reqwest_client(&self) -> Result<reqwest::Client> { + if let Some(client_cert_path) = self.client_cert_path.as_ref() { + let mut buf = Vec::new(); + let mut f = tokio::fs::File::open(client_cert_path) + .await + .map_err(|e| Error::LoadClientCert { + source: e, + file: client_cert_path.clone(), + })?; + f.read_to_end(&mut buf).await.map_err(|e| { + Error::LoadClientCert { + source: e, + file: client_cert_path.clone(), + } + })?; + let pem = reqwest::Identity::from_pem(&buf) + .map_err(|e| Error::CreateReqwestClient { source: e })?; + Ok(reqwest::Client::builder() + .user_agent(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .identity(pem) + .build() + .map_err(|e| Error::CreateReqwestClient { source: e })?) + } else { + Ok(reqwest::Client::builder() + .user_agent(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .build() + .map_err(|e| Error::CreateReqwestClient { source: e })?) } } - pub async fn prelogin(&self, email: &str) -> Result<u32> { + pub async fn prelogin( + &self, + email: &str, + ) -> Result<(KdfType, u32, Option<u32>, Option<u32>)> { let prelogin = PreloginReq { email: email.to_string(), }; - let client = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .post(&self.api_url("/accounts/prelogin")) .json(&prelogin) @@ -573,7 +809,12 @@ impl Client { .await .map_err(|source| Error::Reqwest { source })?; let prelogin_res: PreloginRes = res.json_with_path().await?; - Ok(prelogin_res.kdf_iterations) + Ok(( + prelogin_res.kdf, + prelogin_res.kdf_iterations, + prelogin_res.kdf_memory, + prelogin_res.kdf_parallelism, + )) } pub async fn register( @@ -596,22 +837,34 @@ impl Client { device_type: 8, device_identifier: device_id.to_string(), device_name: "rbw".to_string(), - device_push_token: "".to_string(), + device_push_token: String::new(), two_factor_token: None, two_factor_provider: None, }; - let client = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) .send() .await .map_err(|source| Error::Reqwest { source })?; - if let reqwest::StatusCode::OK = res.status() { + if res.status() == reqwest::StatusCode::OK { Ok(()) } else { let code = res.status().as_u16(); - Err(classify_login_error(&res.json_with_path().await?, code)) + match res.text().await { + Ok(body) => match body.clone().json_with_path() { + Ok(json) => Err(classify_login_error(&json, code)), + Err(e) => { + log::warn!("{e}: {body}"); + Err(Error::RequestFailed { status: code }) + } + }, + Err(e) => { + log::warn!("failed to read response body: {e}"); + Err(Error::RequestFailed { status: code }) + } + } } } @@ -626,30 +879,30 @@ impl Client { let connect_req = ConnectPasswordReq { grant_type: "password".to_string(), username: email.to_string(), - password: Some(base64::encode(password_hash.hash())), + password: Some(crate::base64::encode(password_hash.hash())), scope: "api offline_access".to_string(), client_id: "desktop".to_string(), client_secret: None, device_type: 8, device_identifier: device_id.to_string(), device_name: "rbw".to_string(), - device_push_token: "".to_string(), + device_push_token: String::new(), two_factor_token: two_factor_token .map(std::string::ToString::to_string), two_factor_provider: two_factor_provider.map(|ty| ty as u32), }; - let client = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) .header( "auth-email", - base64::encode_config(email, base64::URL_SAFE_NO_PAD), + crate::base64::encode_url_safe_no_pad(email), ) .send() .await .map_err(|source| Error::Reqwest { source })?; - if let reqwest::StatusCode::OK = res.status() { + if res.status() == reqwest::StatusCode::OK { let connect_res: ConnectPasswordRes = res.json_with_path().await?; Ok(( @@ -659,7 +912,19 @@ impl Client { )) } else { let code = res.status().as_u16(); - Err(classify_login_error(&res.json_with_path().await?, code)) + match res.text().await { + Ok(body) => match body.clone().json_with_path() { + Ok(json) => Err(classify_login_error(&json, code)), + Err(e) => { + log::warn!("{e}: {body}"); + Err(Error::RequestFailed { status: code }) + } + }, + Err(e) => { + log::warn!("failed to read response body: {e}"); + Err(Error::RequestFailed { status: code }) + } + } } } @@ -672,10 +937,10 @@ impl Client { std::collections::HashMap<String, String>, Vec<crate::db::Entry>, )> { - let client = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .get(&self.api_url("/sync")) - .header("Authorization", format!("Bearer {}", access_token)) + .header("Authorization", format!("Bearer {access_token}")) .send() .await .map_err(|source| Error::Reqwest { source })?; @@ -816,8 +1081,8 @@ impl Client { } let client = reqwest::blocking::Client::new(); let res = client - .post(&self.api_url("/ciphers")) - .header("Authorization", format!("Bearer {}", access_token)) + .post(self.api_url("/ciphers")) + .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; @@ -839,12 +1104,18 @@ impl Client { org_id: Option<&str>, name: &str, data: &crate::db::EntryData, + fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<()> { let mut req = CiphersPutReq { - ty: 1, + ty: match data { + crate::db::EntryData::Login { .. } => 1, + crate::db::EntryData::SecureNote { .. } => 2, + crate::db::EntryData::Card { .. } => 3, + crate::db::EntryData::Identity { .. } => 4, + }, folder_id: folder_uuid.map(std::string::ToString::to_string), organization_id: org_id.map(std::string::ToString::to_string), name: name.to_string(), @@ -853,6 +1124,15 @@ impl Client { card: None, identity: None, secure_note: None, + fields: fields + .iter() + .map(|field| CipherField { + ty: field.ty, + name: field.name.clone(), + value: field.value.clone(), + linked_id: field.linked_id, + }) + .collect(), password_history: history .iter() .map(|entry| CiphersPutReqHistory { @@ -949,8 +1229,8 @@ impl Client { } let client = reqwest::blocking::Client::new(); let res = client - .put(&self.api_url(&format!("/ciphers/{}", id))) - .header("Authorization", format!("Bearer {}", access_token)) + .put(self.api_url(&format!("/ciphers/{id}"))) + .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; @@ -968,8 +1248,8 @@ impl Client { pub fn remove(&self, access_token: &str, id: &str) -> Result<()> { let client = reqwest::blocking::Client::new(); let res = client - .delete(&self.api_url(&format!("/ciphers/{}", id))) - .header("Authorization", format!("Bearer {}", access_token)) + .delete(self.api_url(&format!("/ciphers/{id}"))) + .header("Authorization", format!("Bearer {access_token}")) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { @@ -989,8 +1269,8 @@ impl Client { ) -> Result<Vec<(String, String)>> { let client = reqwest::blocking::Client::new(); let res = client - .get(&self.api_url("/folders")) - .header("Authorization", format!("Bearer {}", access_token)) + .get(self.api_url("/folders")) + .header("Authorization", format!("Bearer {access_token}")) .send() .map_err(|source| Error::Reqwest { source })?; match res.status() { @@ -1021,8 +1301,8 @@ impl Client { }; let client = reqwest::blocking::Client::new(); let res = client - .post(&self.api_url("/folders")) - .header("Authorization", format!("Bearer {}", access_token)) + .post(self.api_url("/folders")) + .header("Authorization", format!("Bearer {access_token}")) .json(&req) .send() .map_err(|source| Error::Reqwest { source })?; @@ -1051,7 +1331,7 @@ impl Client { }; let client = reqwest::blocking::Client::new(); let res = client - .post(&self.identity_url("/connect/token")) + .post(self.identity_url("/connect/token")) .form(&connect_req) .send() .map_err(|source| Error::Reqwest { source })?; @@ -1068,7 +1348,7 @@ impl Client { client_id: "desktop".to_string(), refresh_token: refresh_token.to_string(), }; - let client = reqwest::Client::new(); + let client = self.reqwest_client().await?; let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) |