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