#![allow(clippy::as_conversions)]
use crate::prelude::*;
use crate::json::{
DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync 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<'de> serde::Deserialize<'de> for TwoFactorProviderType {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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<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(TwoFactorProviderTypeVisitor)
}
}
impl std::convert::TryFrom<u64> for TwoFactorProviderType {
type Error = Error;
fn try_from(ty: u64) -> Result<Self> {
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<Self> {
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(serde::Serialize, Debug)]
struct PreloginReq {
email: String,
}
#[derive(serde::Deserialize, Debug)]
struct PreloginRes {
#[serde(rename = "KdfIterations", alias = "kdfIterations")]
kdf_iterations: u32,
}
#[derive(serde::Serialize, Debug)]
struct ConnectPasswordReq {
grant_type: String,
username: String,
password: Option<String>,
scope: String,
client_id: String,
client_secret: Option<String>,
#[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<String>,
#[serde(rename = "twoFactorProvider")]
two_factor_provider: Option<u32>,
}
#[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<String>,
#[serde(rename = "ErrorModel", alias = "errorModel")]
error_model: Option<ConnectErrorResErrorModel>,
#[serde(rename = "TwoFactorProviders", alias = "twoFactorProviders")]
two_factor_providers: Option<Vec<TwoFactorProviderType>>,
}
#[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<SyncResCipher>,
#[serde(rename = "Profile", alias = "profile")]
profile: SyncResProfile,
#[serde(rename = "Folders", alias = "folders")]
folders: Vec<SyncResFolder>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct SyncResCipher {
#[serde(rename = "Id", alias = "id")]
id: String,
#[serde(rename = "FolderId", alias = "folderId")]
folder_id: Option<String>,
#[serde(rename = "OrganizationId", alias = "organizationId")]
organization_id: Option<String>,
#[serde(rename = "Name", alias = "name")]
name: String,
#[serde(rename = "Login", alias = "login")]
login: Option<CipherLogin>,
#[serde(rename = "Card", alias = "card")]
card: Option<CipherCard>,
#[serde(rename = "Identity", alias = "identity")]
identity: Option<CipherIdentity>,
#[serde(rename = "SecureNote", alias = "secureNote")]
secure_note: Option<CipherSecureNote>,
#[serde(rename = "Notes", alias = "notes")]
notes: Option<String>,
#[serde(rename = "PasswordHistory", alias = "passwordHistory")]
password_history: Option<Vec<SyncResPasswordHistory>>,
#[serde(rename = "Fields", alias = "fields")]
fields: Option<Vec<SyncResField>>,
#[serde(rename = "DeletedDate", alias = "deletedDate")]
deleted_date: Option<String>,
}
impl SyncResCipher {
fn to_entry(
&self,
folders: &[SyncResFolder],
) -> Option<crate::db::Entry> {
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| {
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 {
name: field.name.clone(),
value: field.value.clone(),
})
.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<SyncResProfileOrganization>,
}
#[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<String>,
#[serde(rename = "Password", alias = "password")]
password: Option<String>,
#[serde(rename = "Totp", alias = "totp")]
totp: Option<String>,
#[serde(rename = "Uris", alias = "uris")]
uris: Option<Vec<CipherLoginUri>>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct CipherLoginUri {
#[serde(rename = "Uri", alias = "uri")]
uri: Option<String>,
#[serde(rename = "Match", alias = "match")]
match_type: Option<UriMatchType>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct CipherCard {
#[serde(rename = "CardholderName", alias = "cardHolderName")]
cardholder_name: Option<String>,
#[serde(rename = "Number", alias = "number")]
number: Option<String>,
#[serde(rename = "Brand", alias = "brand")]
brand: Option<String>,
#[serde(rename = "ExpMonth", alias = "expMonth")]
exp_month: Option<String>,
#[serde(rename = "ExpYear", alias = "expYear")]
exp_year: Option<String>,
#[serde(rename = "Code", alias = "code")]
code: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct CipherIdentity {
#[serde(rename = "Title", alias = "title")]
title: Option<String>,
#[serde(rename = "FirstName", alias = "firstName")]
first_name: Option<String>,
#[serde(rename = "MiddleName", alias = "middleName")]
middle_name: Option<String>,
#[serde(rename = "LastName", alias = "lastName")]
last_name: Option<String>,
#[serde(rename = "Address1", alias = "address1")]
address1: Option<String>,
#[serde(rename = "Address2", alias = "address2")]
address2: Option<String>,
#[serde(rename = "Address3", alias = "address3")]
address3: Option<String>,
#[serde(rename = "City", alias = "city")]
city: Option<String>,
#[serde(rename = "State", alias = "state")]
state: Option<String>,
#[serde(rename = "PostalCode", alias = "postalCode")]
postal_code: Option<String>,
#[serde(rename = "Country", alias = "country")]
country: Option<String>,
#[serde(rename = "Phone", alias = "phone")]
phone: Option<String>,
#[serde(rename = "Email", alias = "email")]
email: Option<String>,
#[serde(rename = "SSN", alias = "ssn")]
ssn: Option<String>,
#[serde(rename = "LicenseNumber", alias = "licenseNumber")]
license_number: Option<String>,
#[serde(rename = "PassportNumber", alias = "passportNumber")]
passport_number: Option<String>,
#[serde(rename = "Username", alias = "username")]
username: Option<String>,
}
#[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<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct SyncResField {
#[serde(rename = "Type", alias = "type")]
ty: u32,
#[serde(rename = "Name", alias = "name")]
name: Option<String>,
#[serde(rename = "Value", alias = "value")]
value: Option<String>,
}
#[derive(serde::Serialize, Debug)]
struct CiphersPostReq {
#[serde(rename = "type")]
ty: u32, #[serde(rename = "folderId")]
folder_id: Option<String>,
name: String,
notes: Option<String>,
login: Option<CipherLogin>,
card: Option<CipherCard>,
identity: Option<CipherIdentity>,
#[serde(rename = "secureNote")]
secure_note: Option<CipherSecureNote>,
}
#[derive(serde::Serialize, Debug)]
struct CiphersPutReq {
#[serde(rename = "type")]
ty: u32, #[serde(rename = "folderId")]
folder_id: Option<String>,
#[serde(rename = "organizationId")]
organization_id: Option<String>,
name: String,
notes: Option<String>,
login: Option<CipherLogin>,
card: Option<CipherCard>,
identity: Option<CipherIdentity>,
#[serde(rename = "secureNote")]
secure_note: Option<CipherSecureNote>,
#[serde(rename = "passwordHistory")]
password_history: Vec<CiphersPutReqHistory>,
}
#[derive(serde::Serialize, Debug)]
struct CiphersPutReqLogin {
username: Option<String>,
password: Option<String>,
}
#[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<FoldersResData>,
}
#[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,
}
impl Client {
#[must_use]
pub fn new(base_url: &str, identity_url: &str) -> Self {
Self {
base_url: base_url.to_string(),
identity_url: identity_url.to_string(),
}
}
pub async fn prelogin(&self, email: &str) -> Result<u32> {
let prelogin = PreloginReq {
email: email.to_string(),
};
let client = reqwest::Client::new();
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_iterations)
}
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(),
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: "".to_string(),
two_factor_token: None,
two_factor_provider: None,
};
let client = reqwest::Client::new();
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();
Err(classify_login_error(&res.json_with_path().await?, 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<TwoFactorProviderType>,
) -> Result<(String, String, String)> {
let connect_req = ConnectPasswordReq {
grant_type: "password".to_string(),
username: email.to_string(),
password: Some(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(),
two_factor_token: two_factor_token
.map(std::string::ToString::to_string),
#[allow(clippy::as_conversions)]
two_factor_provider: two_factor_provider.map(|ty| ty as u32),
};
let client = reqwest::Client::new();
let res = client
.post(&self.identity_url("/connect/token"))
.form(&connect_req)
.header(
"auth-email",
base64::encode_config(email, base64::URL_SAFE_NO_PAD),
)
.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();
Err(classify_login_error(&res.json_with_path().await?, code))
}
}
pub async fn sync(
&self,
access_token: &str,
) -> Result<(
String,
String,
std::collections::HashMap<String, String>,
Vec<crate::db::Entry>,
)> {
let client = reqwest::Client::new();
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,
notes: Option<&str>,
folder_uuid: Option<&str>,
history: &[crate::db::HistoryEntry],
) -> Result<()> {
let mut req = CiphersPutReq {
ty: 1,
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,
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<Vec<(String, String)>> {
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<String> {
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<String> {
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<String> {
let connect_req = ConnectRefreshTokenReq {
grant_type: "refresh_token".to_string(),
client_id: "desktop".to_string(),
refresh_token: refresh_token.to_string(),
};
let client = reqwest::Client::new();
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;
}
"" => {
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 }
}